From b593e818e8e78894c18bd83e9baceebe14888daa Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:06:06 +0200 Subject: [PATCH 01/48] Initial commit --- .gitignore | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7bbc71c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,101 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..119161be --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Tom van Ommeren + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 3020e309428cdad918718621035a1322c4c9f048 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:08:47 +0200 Subject: [PATCH 02/48] initial commit --- pyls_mypy/__init__.py | 7 + pyls_mypy/_version.py | 520 ++++++++++++ pyls_mypy/plugin.py | 38 + requirements.txt | 4 + setup.cfg | 7 + setup.py | 55 ++ test/__init__.py | 0 test/test_plugin.py | 29 + versioneer.py | 1822 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 2482 insertions(+) create mode 100644 pyls_mypy/__init__.py create mode 100644 pyls_mypy/_version.py create mode 100644 pyls_mypy/plugin.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 test/__init__.py create mode 100644 test/test_plugin.py create mode 100644 versioneer.py diff --git a/pyls_mypy/__init__.py b/pyls_mypy/__init__.py new file mode 100644 index 00000000..34311623 --- /dev/null +++ b/pyls_mypy/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2017 Palantir Technologies, Inc. +from future.standard_library import install_aliases +from ._version import get_versions + +install_aliases() +__version__ = get_versions()['version'] +del get_versions diff --git a/pyls_mypy/_version.py b/pyls_mypy/_version.py new file mode 100644 index 00000000..89b42fc2 --- /dev/null +++ b/pyls_mypy/_version.py @@ -0,0 +1,520 @@ + +# 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} diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py new file mode 100644 index 00000000..104d6a30 --- /dev/null +++ b/pyls_mypy/plugin.py @@ -0,0 +1,38 @@ +# Copyright 2017 Mikael Knutsson +from mypy import api as mypy_api +from pyls import hookimpl + + +@hookimpl +def pyls_lint(document): + args = ('--ignore-missing-imports', + '--incremental', + '--show-column-numbers', + '--command', document.source) + + report, errors, _ = mypy_api.run(args) + diagnostics = [] + for line in report.splitlines(): + split = line.split(':', 4) + if len(split) == 5: + _, lineno, offset, severity, msg = split + else: + _, lineno, severity, msg = split + offset = 0 + lineno = int(lineno) + offset = int(offset) + errno = 2 + if severity.strip() == 'error': + errno = 1 + diagnostics.append({ + 'source': 'mypy', + 'range': { + 'start': {'line': lineno - 1, 'character': offset}, + # There may be a better solution, but mypy does not provide end + 'end': {'line': lineno - 1, 'character': offset + 1} + }, + 'message': msg, + 'severity': errno + }) + + return diagnostics diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..fbed6e80 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +future +configparser +python-language-server +mypy;python_version >= '3.2' diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..383ada03 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[versioneer] +VCS = git +style = pep440 +versionfile_source = pyls_mypy/_version.py +versionfile_build = pyls_mypy/_version.py +tag_prefix = +parentdir_prefix = diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..47c035de --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +from setuptools import find_packages, setup +import versioneer + +# README = open('README.rst', 'r').read() + + +setup( + name='pyls-mypy', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + + description='Mypy linter for the Python Language Server for the Language Server Protocol', + + # long_description=README, + + # The project's main homepage. + url='https://github.com/tomv564/pyls-mypy', + + author='Tom van Ommeren', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'test']), + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + 'future', + 'configparser', + 'python-language-server', + 'mypy' + ], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[test] + extras_require={ + ':python_version >= "3.2"': ['mypy'], + 'test': ['tox', 'versioneer', 'pytest', 'pytest-cov', 'coverage'], + }, + + entry_points={ + 'pyls': [ + 'pyls_mypy = pyls_mypy.plugin', + ], + }, +) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_plugin.py b/test/test_plugin.py new file mode 100644 index 00000000..8ece611b --- /dev/null +++ b/test/test_plugin.py @@ -0,0 +1,29 @@ +# Copyright 2017 Palantir Technologies, Inc. +from pyls.workspace import Document +from pyls_mypy import plugin + +DOC_URI = __file__ +DOC = """import sys + +def hello(): +\tpass + +import json +""" + +DOC_SYNTAX_ERR = """def hello() + pass +""" + +DOC_TYPE_ERR = """{}.append(3) +""" + +def test_plugin(): + doc = Document(DOC_URI, DOC_TYPE_ERR) + diags = plugin.pyls_lint(doc) + + assert len(diags) == 1 + diag = diags[0] + assert diag['message'] == ' Dict[, ] has no attribute "append"' + assert diag['range']['start'] == {'line': 0, 'character': 0} + assert diag['range']['end'] == {'line': 0, 'character': 1} diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 00000000..64fea1c8 --- /dev/null +++ b/versioneer.py @@ -0,0 +1,1822 @@ + +# 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 300cafc7d05e8e4595ac904b3192b87c913cbda1 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:13:20 +0200 Subject: [PATCH 03/48] Add readme --- setup.py | 4 ++-- untitled.rst | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 untitled.rst diff --git a/setup.py b/setup.py index 47c035de..f4755a3a 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup import versioneer -# README = open('README.rst', 'r').read() +README = open('README.rst', 'r').read() setup( @@ -16,7 +16,7 @@ description='Mypy linter for the Python Language Server for the Language Server Protocol', - # long_description=README, + long_description=README, # The project's main homepage. url='https://github.com/tomv564/pyls-mypy', diff --git a/untitled.rst b/untitled.rst new file mode 100644 index 00000000..69ae05c8 --- /dev/null +++ b/untitled.rst @@ -0,0 +1,13 @@ +Mypy plugin for PYLS +====================== + +https://github.com/palantir/python-language-server + + +Installation +------------ + +Install into the same virtualenv as pyls itself. + +``pip install mypy-pyls`` + From a04eaa98555facf01d3a63ada0b953e2ab8ec452 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:14:56 +0200 Subject: [PATCH 04/48] Rename to readme --- untitled.rst => README.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename untitled.rst => README.rst (100%) diff --git a/untitled.rst b/README.rst similarity index 100% rename from untitled.rst rename to README.rst From ef1b5d4e7b05bd95b0ddf8eab11dff9922ba01eb Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:16:26 +0200 Subject: [PATCH 05/48] Update readme --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 69ae05c8..616474bb 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,9 @@ Mypy plugin for PYLS ====================== -https://github.com/palantir/python-language-server +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. Installation From 630ac5ebe89669c1ece2b0720c0fa629db512b23 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:17:52 +0200 Subject: [PATCH 06/48] Add note about cloning and install until package is published --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 616474bb..15b22aaf 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,9 @@ It, like mypy, requires Python 3.2 or newer. Installation ------------ +This package is not published, yet. +For now you can clone this repository and run ``pip install .`` + Install into the same virtualenv as pyls itself. ``pip install mypy-pyls`` From 93be6ac6bb6ee0b781f9754272a13134b3b3192a Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:34:55 +0200 Subject: [PATCH 07/48] Clean up old test content --- test/test_plugin.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 8ece611b..43cc7468 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -3,18 +3,6 @@ from pyls_mypy import plugin DOC_URI = __file__ -DOC = """import sys - -def hello(): -\tpass - -import json -""" - -DOC_SYNTAX_ERR = """def hello() - pass -""" - DOC_TYPE_ERR = """{}.append(3) """ From a60e7d06368993df7848124b23bae727360d1729 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:35:56 +0200 Subject: [PATCH 08/48] Remove copyright notices --- pyls_mypy/__init__.py | 1 - test/test_plugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyls_mypy/__init__.py b/pyls_mypy/__init__.py index 34311623..3fa6e5ac 100644 --- a/pyls_mypy/__init__.py +++ b/pyls_mypy/__init__.py @@ -1,4 +1,3 @@ -# Copyright 2017 Palantir Technologies, Inc. from future.standard_library import install_aliases from ._version import get_versions diff --git a/test/test_plugin.py b/test/test_plugin.py index 43cc7468..42d5de96 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,4 +1,3 @@ -# Copyright 2017 Palantir Technologies, Inc. from pyls.workspace import Document from pyls_mypy import plugin From 62ebafad1966a08895684fb4c5758468d5206b3f Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:40:46 +0200 Subject: [PATCH 09/48] Remove mypy as optional dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index f4755a3a..f203ddf6 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ # for example: # $ pip install -e .[test] extras_require={ - ':python_version >= "3.2"': ['mypy'], 'test': ['tox', 'versioneer', 'pytest', 'pytest-cov', 'coverage'], }, From 8c81ffb7ed9ce23979c66b5fb0790265fd504eb2 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 15:57:28 +0200 Subject: [PATCH 10/48] Also strip message field --- pyls_mypy/plugin.py | 2 +- test/test_plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 104d6a30..6346b799 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -31,7 +31,7 @@ def pyls_lint(document): # There may be a better solution, but mypy does not provide end 'end': {'line': lineno - 1, 'character': offset + 1} }, - 'message': msg, + 'message': msg.strip(), 'severity': errno }) diff --git a/test/test_plugin.py b/test/test_plugin.py index 42d5de96..1c7ff6a9 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -11,6 +11,6 @@ def test_plugin(): assert len(diags) == 1 diag = diags[0] - assert diag['message'] == ' Dict[, ] has no attribute "append"' + assert diag['message'] == 'Dict[, ] has no attribute "append"' assert diag['range']['start'] == {'line': 0, 'character': 0} assert diag['range']['end'] == {'line': 0, 'character': 1} From be5843ae7a96836114acd96f6b70a5936f52dc1a Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 17:47:24 +0200 Subject: [PATCH 11/48] Parse mypy result with regex --- pyls_mypy/plugin.py | 45 ++++++++++++++++++++++++++------------------- test/test_plugin.py | 10 ++++++++++ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 6346b799..dd07fc42 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -1,38 +1,45 @@ # Copyright 2017 Mikael Knutsson +import re from mypy import api as mypy_api from pyls import hookimpl +line_pattern = r"(.*):(\d+):(\d+): (\w+): (.*)" -@hookimpl -def pyls_lint(document): - args = ('--ignore-missing-imports', - '--incremental', - '--show-column-numbers', - '--command', document.source) - report, errors, _ = mypy_api.run(args) - diagnostics = [] - for line in report.splitlines(): - split = line.split(':', 4) - if len(split) == 5: - _, lineno, offset, severity, msg = split - else: - _, lineno, severity, msg = split - offset = 0 +def parse_line(line): + result = re.match(line_pattern, line) + if result: + _, lineno, offset, severity, msg = result.groups() lineno = int(lineno) offset = int(offset) errno = 2 - if severity.strip() == 'error': + if severity == 'error': errno = 1 - diagnostics.append({ + return { 'source': 'mypy', 'range': { 'start': {'line': lineno - 1, 'character': offset}, # There may be a better solution, but mypy does not provide end 'end': {'line': lineno - 1, 'character': offset + 1} }, - 'message': msg.strip(), + 'message': msg, 'severity': errno - }) + } + + +@hookimpl +def pyls_lint(document): + args = ('--ignore-missing-imports', + '--incremental', + '--show-column-numbers', + '--command', document.source) + + report, errors, _ = mypy_api.run(args) + + diagnostics = [] + for line in report.splitlines(): + diag = parse_line(line) + if diag: + diagnostics.append(diag) return diagnostics diff --git a/test/test_plugin.py b/test/test_plugin.py index 1c7ff6a9..5426cd41 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -5,6 +5,9 @@ DOC_TYPE_ERR = """{}.append(3) """ +TEST_LINE = 'main.py:279:8: error: "Request" has no attribute "id"' + + def test_plugin(): doc = Document(DOC_URI, DOC_TYPE_ERR) diags = plugin.pyls_lint(doc) @@ -14,3 +17,10 @@ def test_plugin(): assert diag['message'] == 'Dict[, ] has no attribute "append"' assert diag['range']['start'] == {'line': 0, 'character': 0} assert diag['range']['end'] == {'line': 0, 'character': 1} + + +def test_parse_line(): + diag = plugin.parse_line(TEST_LINE) + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 278, 'character': 8} + assert diag['range']['end'] == {'line': 278, 'character': 9} From bf163ac02c0f40892f4e1bbd4418e006840df9e1 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 18:27:52 +0200 Subject: [PATCH 12/48] Handle results without line or column --- pyls_mypy/plugin.py | 6 +++--- test/test_plugin.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index dd07fc42..c95f62be 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -3,15 +3,15 @@ from mypy import api as mypy_api from pyls import hookimpl -line_pattern = r"(.*):(\d+):(\d+): (\w+): (.*)" +line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" def parse_line(line): result = re.match(line_pattern, line) if result: _, lineno, offset, severity, msg = result.groups() - lineno = int(lineno) - offset = int(offset) + lineno = int(lineno or 1) + offset = int(offset or 0) errno = 2 if severity == 'error': errno = 1 diff --git a/test/test_plugin.py b/test/test_plugin.py index 5426cd41..72865939 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -6,6 +6,8 @@ """ TEST_LINE = 'main.py:279:8: error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_COL = 'main.py:279: error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_LINE = 'main.py: error: "Request" has no attribute "id"' def test_plugin(): @@ -19,8 +21,22 @@ def test_plugin(): assert diag['range']['end'] == {'line': 0, 'character': 1} -def test_parse_line(): +def test_parse_full_line(): diag = plugin.parse_line(TEST_LINE) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 278, 'character': 8} assert diag['range']['end'] == {'line': 278, 'character': 9} + + +def test_parse_line_without_col(): + diag = plugin.parse_line(TEST_LINE_WITHOUT_COL) + 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(): + diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE) + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 0, 'character': 0} + assert diag['range']['end'] == {'line': 0, 'character': 1} From 888d4f98f8ccf01c7e4368f9c20cd863843b7cef Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 8 Aug 2017 20:09:47 +0200 Subject: [PATCH 13/48] Update installation instructions --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 15b22aaf..64d0b1a6 100644 --- a/README.rst +++ b/README.rst @@ -9,10 +9,7 @@ It, like mypy, requires Python 3.2 or newer. Installation ------------ -This package is not published, yet. -For now you can clone this repository and run ``pip install .`` - Install into the same virtualenv as pyls itself. -``pip install mypy-pyls`` +``pip install pyls-mypy`` From c3b5a04fa76e15a2113a5dde470c8cd665938abe Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 13 Aug 2017 11:16:37 +0200 Subject: [PATCH 14/48] add manifest to include required files --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..7a0a016e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.rst +include versioneer.py +include pyls-mypy/_version.py From e594a57154128e11f5beb2ec06e7420d2fdee5ec Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 29 Aug 2017 08:02:55 +0200 Subject: [PATCH 15/48] Don't default strictness options on command line --- pyls_mypy/plugin.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index c95f62be..bd410157 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -1,4 +1,3 @@ -# Copyright 2017 Mikael Knutsson import re from mypy import api as mypy_api from pyls import hookimpl @@ -29,11 +28,9 @@ def parse_line(line): @hookimpl def pyls_lint(document): - args = ('--ignore-missing-imports', - '--incremental', + args = ('--incremental', '--show-column-numbers', '--command', document.source) - report, errors, _ = mypy_api.run(args) diagnostics = [] From 4c727120d2cbd8bf2825e1491cd55175f03266d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 25 May 2018 10:22:30 +0100 Subject: [PATCH 16/48] message contains now quotes in pyls 0.18.0 Not 100% sure when this was introduced. --- test/test_plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 72865939..db1c7136 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -16,9 +16,9 @@ def test_plugin(): assert len(diags) == 1 diag = diags[0] - assert diag['message'] == 'Dict[, ] has no attribute "append"' - assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 1} + assert diag['message'] == '"Dict[, ]" has no attribute "append"' + assert diag['range']['start'] == {'line': 0, 'character': 1} + assert diag['range']['end'] == {'line': 0, 'character': 2} def test_parse_full_line(): From f6740236ba41aeb4f473d5756e58fa656b5ca711 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 21:27:13 +0200 Subject: [PATCH 17/48] Add travis config --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..e1232f34 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "3.6" +before_install: + - pip install -r requirements.txt +script: + - flake8 + - python -m unittest discover + - pytest + # - coverage run --source=. -m unittest discover +# after_success: + # - coveralls \ No newline at end of file From b86d6c4caf3ea38df3493d66ec138e5c3d77375d Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 21:35:17 +0200 Subject: [PATCH 18/48] Install flake8, remove old unittest cmd --- .travis.yml | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e1232f34..7824ba6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ before_install: - pip install -r requirements.txt script: - flake8 - - python -m unittest discover - pytest # - coverage run --source=. -m unittest discover # after_success: diff --git a/requirements.txt b/requirements.txt index fbed6e80..7e5589b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ future +flake8 configparser python-language-server mypy;python_version >= '3.2' From 5ce0ce47be926e092fdabd095fa37b12bf6289af Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 22:16:38 +0200 Subject: [PATCH 19/48] Fix long lines --- setup.py | 2 +- test/test_plugin.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f203ddf6..8d66eeb7 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), - description='Mypy linter for the Python Language Server for the Language Server Protocol', + description='Mypy linter for the Python Language Server', long_description=README, diff --git a/test/test_plugin.py b/test/test_plugin.py index db1c7136..fb81935f 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -4,6 +4,7 @@ DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) """ +TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' TEST_LINE = 'main.py:279:8: error: "Request" has no attribute "id"' TEST_LINE_WITHOUT_COL = 'main.py:279: error: "Request" has no attribute "id"' @@ -16,7 +17,7 @@ def test_plugin(): assert len(diags) == 1 diag = diags[0] - assert diag['message'] == '"Dict[, ]" has no attribute "append"' + assert diag['message'] == TYPE_ERR_MSG assert diag['range']['start'] == {'line': 0, 'character': 1} assert diag['range']['end'] == {'line': 0, 'character': 2} From 8eee89e0975bbbc5a99827ebd64721e31391e262 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 22:29:30 +0200 Subject: [PATCH 20/48] Add badge --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 64d0b1a6..ae49dcad 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ Mypy plugin for PYLS ====================== +[![Build Status](https://travis-ci.org/tomv564/pyls-mypy.svg?branch=master)](https://travis-ci.org/tomv564/pyls-mypy) + 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. From 9a301f252bd1e91d949c552c57ba7dceebad6d3b Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 22:31:50 +0200 Subject: [PATCH 21/48] Use RST for badge --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ae49dcad..df969c92 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,8 @@ Mypy plugin for PYLS ====================== -[![Build Status](https://travis-ci.org/tomv564/pyls-mypy.svg?branch=master)](https://travis-ci.org/tomv564/pyls-mypy) +.. image:: https://travis-ci.org/tomv564/pyls-mypy.svg?branch=master + :target: https://travis-ci.org/tomv564/pyls-mypy This is a plugin for the Palantir's Python Language Server (https://github.com/palantir/python-language-server) From 1663adab18fb2eaf5ba1f3b98a79a752216aee28 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 22:42:42 +0200 Subject: [PATCH 22/48] Add pypi badge --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index df969c92..db5dc8fd 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,9 @@ Mypy plugin for PYLS ====================== +.. image:: https://badge.fury.io/py/pyls-mypy.svg + :target: https://badge.fury.io/py/pyls-mypy + .. image:: https://travis-ci.org/tomv564/pyls-mypy.svg?branch=master :target: https://travis-ci.org/tomv564/pyls-mypy From 78a7301f21b516b8605686f693a6a8d582046f3a Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 10 Jun 2018 22:49:46 +0200 Subject: [PATCH 23/48] Remove future and configparser from install_requires --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 8d66eeb7..192b17d9 100755 --- a/setup.py +++ b/setup.py @@ -32,8 +32,6 @@ # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=[ - 'future', - 'configparser', 'python-language-server', 'mypy' ], From 985cd8a360d3243c90a8d725c3619126aede6dae Mon Sep 17 00:00:00 2001 From: Rami Chowdhury Date: Sat, 16 Jun 2018 17:39:39 -0400 Subject: [PATCH 24/48] Highlight the word at the point Mypy flags it This provides slightly better context than just the single character at that point. --- pyls_mypy/plugin.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index bd410157..50b843db 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -5,7 +5,11 @@ line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" -def parse_line(line): +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: _, lineno, offset, severity, msg = result.groups() @@ -14,7 +18,7 @@ def parse_line(line): errno = 2 if severity == 'error': errno = 1 - return { + diag = { 'source': 'mypy', 'range': { 'start': {'line': lineno - 1, 'character': offset}, @@ -24,6 +28,15 @@ def parse_line(line): '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 From 8e50bc80a69e66c5d4ba42958cc90abb607bbecd Mon Sep 17 00:00:00 2001 From: Rami Chowdhury Date: Wed, 20 Jun 2018 21:52:46 -0400 Subject: [PATCH 25/48] Add some tests for how word_at_position might react --- test/test_plugin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_plugin.py b/test/test_plugin.py index fb81935f..fdebc81e 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,3 +1,5 @@ +import pytest + from pyls.workspace import Document from pyls_mypy import plugin @@ -41,3 +43,13 @@ def test_parse_line_without_line(): assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 0, 'character': 0} assert diag['range']['end'] == {'line': 0, 'character': 1} + + +@pytest.mark.parametrize('word,bounds', [('', (8, 9)), ('my_var', (8, 14))]) +def test_parse_line_with_context(monkeypatch, word, bounds): + monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) + doc = Document('file:///some/path') + 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]} From 83df547c5036116a411688edf016c0d32d788bd4 Mon Sep 17 00:00:00 2001 From: eliwe Date: Mon, 6 Aug 2018 22:54:54 +0300 Subject: [PATCH 26/48] Add '--follow-imports silent' to mypy invocation Else we get all the errors for all the imported files on the current file. --- pyls_mypy/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 50b843db..5fd058f6 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -43,6 +43,7 @@ def parse_line(line, document=None): def pyls_lint(document): args = ('--incremental', '--show-column-numbers', + '--follow-imports', 'silent', '--command', document.source) report, errors, _ = mypy_api.run(args) From 189833c921d13092ab638812329cab0fac00a9c7 Mon Sep 17 00:00:00 2001 From: Alexey Evseev Date: Fri, 14 Sep 2018 11:04:39 +0300 Subject: [PATCH 27/48] Finalize entire word highlighting --- 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 50b843db..2794fefd 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -48,7 +48,7 @@ def pyls_lint(document): diagnostics = [] for line in report.splitlines(): - diag = parse_line(line) + diag = parse_line(line, document) if diag: diagnostics.append(diag) From d5dc4b3abb655b6443e167556a0b2bd67ffb4b0f Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Mon, 29 Oct 2018 21:51:32 +0100 Subject: [PATCH 28/48] Exclude versioneer from escape code inspection --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7824ba6c..ca2307ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: before_install: - pip install -r requirements.txt script: - - flake8 + - flake8 --exclude=./versioneer.py - pytest # - coverage run --source=. -m unittest discover # after_success: From a595d6a059a6ed6478f46d9e04475499bc94af56 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 30 Oct 2018 00:02:29 +0100 Subject: [PATCH 29/48] Add live_mode=false option to only check saved document --- pyls_mypy/plugin.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 15619484..7146dd27 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -3,7 +3,8 @@ from pyls import hookimpl line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" - +LIVE_MODE = r"live_mode" +DEFAULT_LIVE_MODE = True def parse_line(line, document=None): ''' @@ -40,11 +41,19 @@ def parse_line(line, document=None): @hookimpl -def pyls_lint(document): - args = ('--incremental', - '--show-column-numbers', - '--follow-imports', 'silent', - '--command', document.source) +def pyls_lint(config, document): + live_mode = config.plugin_settings('pyls_mypy').get(LIVE_MODE, DEFAULT_LIVE_MODE) + if live_mode: + args = ('--incremental', + '--show-column-numbers', + '--follow-imports', 'silent', + '--command', document.source) + else: + args = ('--incremental', + '--show-column-numbers', + '--follow-imports', 'silent', + document.path) + report, errors, _ = mypy_api.run(args) diagnostics = [] From 0e7ec6050b99078053756b18602b849cb02026c2 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 30 Oct 2018 00:29:54 +0100 Subject: [PATCH 30/48] Fix test --- test/test_plugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index fdebc81e..68bb450e 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -13,9 +13,15 @@ TEST_LINE_WITHOUT_LINE = 'main.py: error: "Request" has no attribute "id"' +class FakeConfig(object): + def plugin_settings(self, plugin, document_path=None): + return {} + + def test_plugin(): + config = FakeConfig() doc = Document(DOC_URI, DOC_TYPE_ERR) - diags = plugin.pyls_lint(doc) + diags = plugin.pyls_lint(config, doc) assert len(diags) == 1 diag = diags[0] From c70ca23e31804b4ab10b1da0a35aa19b44bb2d6a Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 30 Oct 2018 00:39:13 +0100 Subject: [PATCH 31/48] lint fixes --- pyls_mypy/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 7146dd27..3c2722a0 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -3,8 +3,7 @@ from pyls import hookimpl line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" -LIVE_MODE = r"live_mode" -DEFAULT_LIVE_MODE = True + def parse_line(line, document=None): ''' @@ -42,7 +41,7 @@ def parse_line(line, document=None): @hookimpl def pyls_lint(config, document): - live_mode = config.plugin_settings('pyls_mypy').get(LIVE_MODE, DEFAULT_LIVE_MODE) + live_mode = config.plugin_settings('pyls_mypy').get('live_mode', True) if live_mode: args = ('--incremental', '--show-column-numbers', From d3e49913947e35478afe09ef7a93108d6a872bac Mon Sep 17 00:00:00 2001 From: Belousow Makc Date: Fri, 12 Apr 2019 23:41:00 +0300 Subject: [PATCH 32/48] Fix mypy==0.700 compatibility. mypy.api.run method should take List[str] of args. https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_args https://github.com/python/mypy/blob/d40990f5b91b1f06918222d1041441b7ff63f79a/mypy/api.py#L71 --- pyls_mypy/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 3c2722a0..4db9ef34 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -43,15 +43,15 @@ def parse_line(line, document=None): def pyls_lint(config, document): live_mode = config.plugin_settings('pyls_mypy').get('live_mode', True) if live_mode: - args = ('--incremental', + args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', - '--command', document.source) + '--command', document.source] else: - args = ('--incremental', + args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', - document.path) + document.path] report, errors, _ = mypy_api.run(args) From 0cac94dcf0f7b905461db206a0c16f03261c7b1c Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Mon, 16 Sep 2019 16:51:06 +0200 Subject: [PATCH 33/48] Exclude errors from other files when using follow-imports Seeing this with disallow_untyped_defs=True for example. Test on Windows before releasing? --- pyls_mypy/plugin.py | 15 ++++++++++++++- test/test_plugin.py | 19 ++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 4db9ef34..805f1f11 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -1,9 +1,12 @@ import re +import logging from mypy import api as mypy_api from pyls import hookimpl line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" +log = logging.getLogger(__name__) + def parse_line(line, document=None): ''' @@ -12,7 +15,17 @@ def parse_line(line, document=None): ''' result = re.match(line_pattern, line) if result: - _, lineno, offset, severity, msg = result.groups() + 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) offset = int(offset or 0) errno = 2 diff --git a/test/test_plugin.py b/test/test_plugin.py index 68bb450e..d4a89577 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -8,9 +8,11 @@ """ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' -TEST_LINE = 'main.py:279:8: error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_COL = 'main.py:279: error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_LINE = 'main.py: error: "Request" has no attribute "id"' +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"') class FakeConfig(object): @@ -31,21 +33,24 @@ def test_plugin(): def test_parse_full_line(): - diag = plugin.parse_line(TEST_LINE) + doc = Document(DOC_URI, DOC_TYPE_ERR) + diag = plugin.parse_line(TEST_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 278, 'character': 8} assert diag['range']['end'] == {'line': 278, 'character': 9} def test_parse_line_without_col(): - diag = plugin.parse_line(TEST_LINE_WITHOUT_COL) + doc = Document(DOC_URI, DOC_TYPE_ERR) + 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(): - diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE) + doc = Document(DOC_URI, DOC_TYPE_ERR) + 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} @@ -53,8 +58,8 @@ def test_parse_line_without_line(): @pytest.mark.parametrize('word,bounds', [('', (8, 9)), ('my_var', (8, 14))]) def test_parse_line_with_context(monkeypatch, word, bounds): + doc = Document(DOC_URI, 'DOC_TYPE_ERR') monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) - doc = Document('file:///some/path') diag = plugin.parse_line(TEST_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 278, 'character': bounds[0]} From 83aa8720956bfbbb09152a2ef64ba35c280671d8 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Tue, 1 Oct 2019 17:43:04 +0200 Subject: [PATCH 34/48] Update README.rst --- README.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.rst b/README.rst index db5dc8fd..553b85db 100644 --- a/README.rst +++ b/README.rst @@ -19,3 +19,27 @@ Install into the same virtualenv as pyls itself. ``pip install pyls-mypy`` +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 + +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: +.. code-block:: + "pyls": + { + "plugins": + { + "pyls_mypy": + { + "enabled": true, + "live_mode": false + } + } + } From 3cf99739af7dd9132de72d5462eb4fcf80b3b788 Mon Sep 17 00:00:00 2001 From: AmjadHD Date: Wed, 2 Oct 2019 22:12:13 +0100 Subject: [PATCH 35/48] Fix diagnostics start position --- pyls_mypy/plugin.py | 8 ++++---- test/test_plugin.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 805f1f11..9e7d4ac0 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -26,17 +26,17 @@ def parse_line(line, document=None): document.path) return None - lineno = int(lineno or 1) - offset = int(offset or 0) + 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 - 1, 'character': offset}, + 'start': {'line': lineno, 'character': offset}, # There may be a better solution, but mypy does not provide end - 'end': {'line': lineno - 1, 'character': offset + 1} + 'end': {'line': lineno, 'character': offset + 1} }, 'message': msg, 'severity': errno diff --git a/test/test_plugin.py b/test/test_plugin.py index d4a89577..9263a09b 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -28,16 +28,16 @@ def test_plugin(): assert len(diags) == 1 diag = diags[0] assert diag['message'] == TYPE_ERR_MSG - assert diag['range']['start'] == {'line': 0, 'character': 1} - assert diag['range']['end'] == {'line': 0, 'character': 2} + assert diag['range']['start'] == {'line': 0, 'character': 0} + assert diag['range']['end'] == {'line': 0, 'character': 1} def test_parse_full_line(): doc = Document(DOC_URI, DOC_TYPE_ERR) diag = plugin.parse_line(TEST_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': 8} - assert diag['range']['end'] == {'line': 278, 'character': 9} + assert diag['range']['start'] == {'line': 278, 'character': 7} + assert diag['range']['end'] == {'line': 278, 'character': 8} def test_parse_line_without_col(): @@ -56,7 +56,7 @@ def test_parse_line_without_line(): assert diag['range']['end'] == {'line': 0, 'character': 1} -@pytest.mark.parametrize('word,bounds', [('', (8, 9)), ('my_var', (8, 14))]) +@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') monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) From 8447538886be79913bcae835e5c1622f15326167 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Thu, 3 Oct 2019 23:39:47 +0200 Subject: [PATCH 36/48] When live_mode is off, only lint when document is saved --- pyls_mypy/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 9e7d4ac0..daa7b875 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -53,18 +53,20 @@ def parse_line(line, document=None): @hookimpl -def pyls_lint(config, document): +def pyls_lint(config, workspace, document, is_saved): live_mode = config.plugin_settings('pyls_mypy').get('live_mode', True) if live_mode: args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', '--command', document.source] - else: + elif is_saved: args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', document.path] + else: + return [] report, errors, _ = mypy_api.run(args) From f719b12ee83d9cc9b1fc2702e5fa8ea7c232ea62 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Fri, 4 Oct 2019 18:43:18 +0300 Subject: [PATCH 37/48] Moved the metadata to setup.cfg --- setup.cfg | 29 +++++++++++++++++++++++++++++ setup.py | 52 +++------------------------------------------------- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/setup.cfg b/setup.cfg index 383ada03..404f62ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,26 @@ +[metadata] +name = pyls-mypy +author = Tom van Ommeren +description = Mypy linter for the Python Language Server +url = https://github.com/tomv564/pyls-mypy +long_description = file: README.rst + +[options] +packages = find: +install_requires = python-language-server; mypy + + +[options.entry_points] +pyls = pyls_mypy = pyls_mypy.plugin + +[options.extras_require] +test = + tox + versioneer + pytest + pytest-cov + coverage + [versioneer] VCS = git style = pep440 @@ -5,3 +28,9 @@ versionfile_source = pyls_mypy/_version.py versionfile_build = pyls_mypy/_version.py tag_prefix = parentdir_prefix = + +[options.packages.find] +exclude = + contrib + docs + test \ No newline at end of file diff --git a/setup.py b/setup.py index 192b17d9..d69ef61f 100755 --- a/setup.py +++ b/setup.py @@ -1,52 +1,6 @@ #!/usr/bin/env python -from setuptools import find_packages, setup +from setuptools import setup import versioneer -README = open('README.rst', 'r').read() - - -setup( - name='pyls-mypy', - - # Versions should comply with PEP440. For a discussion on single-sourcing - # the version across setup.py and the project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - - description='Mypy linter for the Python Language Server', - - long_description=README, - - # The project's main homepage. - url='https://github.com/tomv564/pyls-mypy', - - author='Tom van Ommeren', - - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - packages=find_packages(exclude=['contrib', 'docs', 'test']), - - # List run-time dependencies here. These will be installed by pip when - # your project is installed. For an analysis of "install_requires" vs pip's - # requirements files see: - # https://packaging.python.org/en/latest/requirements.html - install_requires=[ - 'python-language-server', - 'mypy' - ], - - # List additional groups of dependencies here (e.g. development - # dependencies). You can install these using the following syntax, - # for example: - # $ pip install -e .[test] - extras_require={ - 'test': ['tox', 'versioneer', 'pytest', 'pytest-cov', 'coverage'], - }, - - entry_points={ - 'pyls': [ - 'pyls_mypy = pyls_mypy.plugin', - ], - }, -) +if __name__ == "__main__": + setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass()) From c1f2f551f570e856fdaa4c2394c5d3b64d86ff32 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sat, 5 Oct 2019 16:20:40 +0200 Subject: [PATCH 38/48] Fix 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 9263a09b..6e13ae3c 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -23,7 +23,8 @@ def plugin_settings(self, plugin, document_path=None): def test_plugin(): config = FakeConfig() doc = Document(DOC_URI, DOC_TYPE_ERR) - diags = plugin.pyls_lint(config, doc) + workspace = None + diags = plugin.pyls_lint(config, workspace, doc, is_saved=False) assert len(diags) == 1 diag = diags[0] From 8ad660a3bc18d9c4f0109f8797af2e4434797c42 Mon Sep 17 00:00:00 2001 From: Tom van Ommeren Date: Sun, 6 Oct 2019 00:15:41 +0200 Subject: [PATCH 39/48] Fix readme code block --- README.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 553b85db..50e6ba57 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Install into the same virtualenv as pyls itself. Configuration ------------- -``live_mode`` (default is True) provides type checking as you type. +``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 @@ -31,15 +31,17 @@ As mypy is unaware of what file path is being checked, there are limitations wit 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: -.. code-block:: + +:: + "pyls": - { - "plugins": - { - "pyls_mypy": - { - "enabled": true, - "live_mode": false - } - } - } + { + "plugins": + { + "pyls_mypy": + { + "enabled": true, + "live_mode": false + } + } + } From c714fa3af75fb7c8f33ba695243eed7cdb332a1b Mon Sep 17 00:00:00 2001 From: Edoardo Morandi Date: Mon, 21 Oct 2019 12:34:47 +0200 Subject: [PATCH 40/48] Added support for 'strict' setting --- pyls_mypy/plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index daa7b875..530dea9d 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -54,7 +54,8 @@ def parse_line(line, document=None): @hookimpl def pyls_lint(config, workspace, document, is_saved): - live_mode = config.plugin_settings('pyls_mypy').get('live_mode', True) + settings = config.plugin_settings('pyls_mypy') + live_mode = settings.get('live_mode', True) if live_mode: args = ['--incremental', '--show-column-numbers', @@ -68,6 +69,9 @@ def pyls_lint(config, workspace, document, is_saved): else: return [] + if settings.get('strict', False): + args.append('--strict') + report, errors, _ = mypy_api.run(args) diagnostics = [] From 2949582ff5f39b1de51eacc92de6cfacf1b5ab75 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Sun, 15 Mar 2020 01:57:20 +0900 Subject: [PATCH 41/48] Install and use future only if python2 --- pyls_mypy/__init__.py | 7 +++++-- requirements.txt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyls_mypy/__init__.py b/pyls_mypy/__init__.py index 3fa6e5ac..fbe5d98b 100644 --- a/pyls_mypy/__init__.py +++ b/pyls_mypy/__init__.py @@ -1,6 +1,9 @@ -from future.standard_library import install_aliases from ._version import get_versions +import sys + +if sys.version_info[0] < 3: + from future.standard_library import install_aliases + install_aliases() -install_aliases() __version__ = get_versions()['version'] del get_versions diff --git a/requirements.txt b/requirements.txt index 7e5589b6..b6a814be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -future +future;python_version < '3' flake8 configparser python-language-server From 1883b3a1464703125870d57eac09050192c5634e Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Thu, 23 Jul 2020 12:27:13 -0500 Subject: [PATCH 42/48] add mypy plugin in the core plugins of pyls --- README.rst | 2 +- .../plugin.py => pyls/plugins/mypy_lint.py | 2 +- pyls_mypy/__init__.py | 9 - pyls_mypy/_version.py | 520 ------------------ setup.py | 2 + .../test_mypy_lint.py} | 3 +- vscode-client/package.json | 10 + 7 files changed, 15 insertions(+), 533 deletions(-) rename pyls_mypy/plugin.py => pyls/plugins/mypy_lint.py (98%) delete mode 100644 pyls_mypy/__init__.py delete mode 100644 pyls_mypy/_version.py rename test/{test_plugin.py => plugins/test_mypy_lint.py} (98%) diff --git a/README.rst b/README.rst index 227bfea7..3c958cef 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,7 @@ If the respective dependencies are found, the following optional providers will * pydocstyle_ linter for docstring style checking (disabled by default) * autopep8_ for code formatting * YAPF_ for code formatting (preferred over autopep8) +* mypy for type linting Optional providers can be installed using the `extras` syntax. To install YAPF_ formatting for example: @@ -46,7 +47,6 @@ If you get an error similar to ``'install_requires' must be a string or list of ~~~~~~~~~~~~~~~~~ Installing these plugins will add extra functionality to the language server: -* pyls-mypy_ Mypy type checking for Python 3 * pyls-isort_ Isort import sort code formatting * pyls-black_ for code formatting using Black_ diff --git a/pyls_mypy/plugin.py b/pyls/plugins/mypy_lint.py similarity index 98% rename from pyls_mypy/plugin.py rename to pyls/plugins/mypy_lint.py index 530dea9d..ff714b75 100644 --- a/pyls_mypy/plugin.py +++ b/pyls/plugins/mypy_lint.py @@ -54,7 +54,7 @@ def parse_line(line, document=None): @hookimpl def pyls_lint(config, workspace, document, is_saved): - settings = config.plugin_settings('pyls_mypy') + settings = config.plugin_settings('mypy') live_mode = settings.get('live_mode', True) if live_mode: args = ['--incremental', diff --git a/pyls_mypy/__init__.py b/pyls_mypy/__init__.py deleted file mode 100644 index fbe5d98b..00000000 --- a/pyls_mypy/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 89b42fc2..00000000 --- a/pyls_mypy/_version.py +++ /dev/null @@ -1,520 +0,0 @@ - -# 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} diff --git a/setup.py b/setup.py index f3c465db..5cb7b99d 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ 'autopep8', 'flake8>=3.8.0', 'mccabe>=0.6.0,<0.7.0', + 'mypy>=0.782' 'pycodestyle>=2.6.0,<2.7.0', 'pydocstyle>=2.0.0', 'pyflakes>=2.2.0,<2.3.0', @@ -91,6 +92,7 @@ 'jedi_signature_help = pyls.plugins.signature', 'jedi_symbols = pyls.plugins.symbols', 'mccabe = pyls.plugins.mccabe_lint', + 'mypy = pyls.plugins.mypy_lint', 'preload = pyls.plugins.preload_imports', 'pycodestyle = pyls.plugins.pycodestyle_lint', 'pydocstyle = pyls.plugins.pydocstyle_lint', diff --git a/test/test_plugin.py b/test/plugins/test_mypy_lint.py similarity index 98% rename from test/test_plugin.py rename to test/plugins/test_mypy_lint.py index 6e13ae3c..7ec0eef4 100644 --- a/test/test_plugin.py +++ b/test/plugins/test_mypy_lint.py @@ -1,7 +1,6 @@ import pytest - +import pyls.plugins.mypy_lint as plugin from pyls.workspace import Document -from pyls_mypy import plugin DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) diff --git a/vscode-client/package.json b/vscode-client/package.json index 7e4ee59f..4505754a 100644 --- a/vscode-client/package.json +++ b/vscode-client/package.json @@ -115,6 +115,16 @@ "default": 15, "description": "The minimum threshold that triggers warnings about cyclomatic complexity." }, + "pyls.plugins.mypy.enabled": { + "type": "boolean", + "default": false, + "description": "Enable type linting." + }, + "pyls.plugins.mypy.live_mode": { + "type": "boolean", + "default": false, + "description": "Enable live mode type linting." + }, "pyls.plugins.preload.enabled": { "type": "boolean", "default": true, From 454509eac452ccf43835983b00d708decbbf7ee3 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 11:00:51 -0500 Subject: [PATCH 43/48] Avoid loading third party plugin for mypy --- pyls/config/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyls/config/config.py b/pyls/config/config.py index 0e124e2c..3d6864b7 100644 --- a/pyls/config/config.py +++ b/pyls/config/config.py @@ -49,6 +49,10 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): # However I don't want all plugins to have to catch ImportError and re-throw. So here we'll filter # out any entry points that throw ImportError assuming one or more of their dependencies isn't present. for entry_point in pkg_resources.iter_entry_points(PYLS): + if str(entry_point) == 'pyls_mypy': + # Don't load the pyls mypy third party plugin for avoiding + # conflicts + continue try: entry_point.load() except ImportError as e: From ef4699480ee20acd1f1e32201708e70c48c92ae3 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 11:12:41 -0500 Subject: [PATCH 44/48] Unpin mypy version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5cb7b99d..ffaf0a0a 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ 'autopep8', 'flake8>=3.8.0', 'mccabe>=0.6.0,<0.7.0', - 'mypy>=0.782' + 'mypy' 'pycodestyle>=2.6.0,<2.7.0', 'pydocstyle>=2.0.0', 'pyflakes>=2.2.0,<2.3.0', From f12ce3ccfb3687d7c159b074e4984448d4630d01 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 11:14:40 -0500 Subject: [PATCH 45/48] fix typo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ffaf0a0a..b8828924 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ 'autopep8', 'flake8>=3.8.0', 'mccabe>=0.6.0,<0.7.0', - 'mypy' + 'mypy>=0.782', 'pycodestyle>=2.6.0,<2.7.0', 'pydocstyle>=2.0.0', 'pyflakes>=2.2.0,<2.3.0', From 7dd99f576b232858fa774784d8535a78b0ffd218 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 11:38:07 -0500 Subject: [PATCH 46/48] fix tests --- pyls/plugins/mypy_lint.py | 1 - test/plugins/test_mypy_lint.py | 35 ++++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pyls/plugins/mypy_lint.py b/pyls/plugins/mypy_lint.py index ff714b75..bc1dc141 100644 --- a/pyls/plugins/mypy_lint.py +++ b/pyls/plugins/mypy_lint.py @@ -16,7 +16,6 @@ def parse_line(line, document=None): 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. diff --git a/test/plugins/test_mypy_lint.py b/test/plugins/test_mypy_lint.py index 7ec0eef4..b35036ac 100644 --- a/test/plugins/test_mypy_lint.py +++ b/test/plugins/test_mypy_lint.py @@ -7,10 +7,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: ' +TEST_LINE = 'test_mypy_lint.py:279:8: error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_COL = ('test_mypy_lint.py:279: ' 'error: "Request" has no attribute "id"') -TEST_LINE_WITHOUT_LINE = ('test_plugin.py: ' +TEST_LINE_WITHOUT_LINE = ('test_mypy_lint.py: ' 'error: "Request" has no attribute "id"') @@ -19,10 +19,17 @@ def plugin_settings(self, plugin, document_path=None): return {} -def test_plugin(): +@pytest.fixture +def tmp_workspace(temp_workspace_factory): + return temp_workspace_factory({ + DOC_URI: DOC_TYPE_ERR + }) + + +def test_plugin(tmp_workspace): config = FakeConfig() - doc = Document(DOC_URI, DOC_TYPE_ERR) - workspace = None + doc = Document(DOC_URI, tmp_workspace, DOC_TYPE_ERR) + workspace = tmp_workspace diags = plugin.pyls_lint(config, workspace, doc, is_saved=False) assert len(diags) == 1 @@ -32,24 +39,24 @@ 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(tmp_workspace): + doc = Document(DOC_URI, tmp_workspace, DOC_TYPE_ERR) 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(tmp_workspace): + doc = Document(DOC_URI, tmp_workspace, DOC_TYPE_ERR) 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(tmp_workspace): + doc = Document(DOC_URI, tmp_workspace, DOC_TYPE_ERR) 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} @@ -57,8 +64,8 @@ def test_parse_line_without_line(): @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(tmp_workspace, monkeypatch, word, bounds): + doc = Document(DOC_URI, tmp_workspace, 'DOC_TYPE_ERR') 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 68f9afcb2df0fd1924a57fb733d0cbb01ff6b992 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 12:02:30 -0500 Subject: [PATCH 47/48] remove python 2 from matrix in appveyor --- appveyor.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4950b8ab..b5e03435 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,10 +2,6 @@ environment: global: APPVEYOR_RDP_PASSWORD: "dcca4c4863E30d56c2e0dda6327370b3#" matrix: - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7.15" - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36" PYTHON_VERSION: "3.6.8" PYTHON_ARCH: "64" From c795881eb30e32472950fef8f2732178d71778b3 Mon Sep 17 00:00:00 2001 From: Stephannie Jimenez Date: Mon, 3 Aug 2020 12:08:59 -0500 Subject: [PATCH 48/48] move tests to python 3 in circleci --- .circleci/config.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4beef059..1554b331 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,11 +6,7 @@ jobs: - image: "python:2.7-stretch" steps: - checkout - - run: pip install -e .[all] .[test] - - run: py.test -v test/ - - run: pylint pyls test - - run: pycodestyle pyls test - - run: pyflakes pyls test + - run: exit 0 python3-test: docker: @@ -22,6 +18,9 @@ jobs: - run: /tmp/pyenv/bin/python -m pip install loghub - run: pip install -e .[all] .[test] - run: py.test -v test/ + - run: pylint pyls test + - run: pycodestyle pyls test + - run: pyflakes pyls test lint: docker: