Skip to content

Commit bc8cad7

Browse files
committed
First commit
0 parents  commit bc8cad7

File tree

11 files changed

+314
-0
lines changed

11 files changed

+314
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.mypy_cache
2+
*.egg-info
3+
*.pyc
4+
.pytest_cache
5+
.ropeproject

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Rupert Bedford
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# pyls-black
2+
3+
> [Black](https://github.com/ambv/black) plugin for the [Python Language Server](https://github.com/palantir/python-language-server/tree/develop/pyls).
4+
5+
```shell
6+
pip3 install pyls-black
7+
```
8+
9+
`pyls-black` can either format an entire file or just the selected text.
10+
The code will only be formatted if it is syntactically valid Python.
11+
Text selections are treated as if they were a separate Python file.
12+
Note that this means you can't format an indented block of code.
13+
14+
## TODO
15+
16+
* Add support for configuring the line length and fast flag.

pyls_black/__init__.py

Whitespace-only changes.

pyls_black/plugin.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import NamedTuple
2+
3+
import black
4+
from pyls import hookimpl
5+
6+
7+
@hookimpl(hookwrapper=True)
8+
def pyls_format_document(document):
9+
return format_document(document)
10+
11+
12+
@hookimpl(hookwrapper=True)
13+
def pyls_format_range(document, range):
14+
selection = Selection(start=range["start"]["line"], end=range["end"]["line"] + 1)
15+
return format_document(document, selection)
16+
17+
18+
class Selection(NamedTuple):
19+
start: int
20+
end: int
21+
22+
def to_range(self):
23+
return {
24+
"start": {"line": self.start, "character": 0},
25+
"end": {"line": self.end, "character": 0},
26+
}
27+
28+
29+
def format_document(document, selection=None):
30+
outcome = yield
31+
32+
result = outcome.get_result()
33+
34+
if result:
35+
text = result[0]["newText"]
36+
else:
37+
text = document.source
38+
39+
if selection:
40+
text = select_text(text, selection)
41+
else:
42+
selection = Selection(0, len(document.lines))
43+
44+
try:
45+
formatted_text = format_text(text)
46+
except (ValueError, black.NothingChanged):
47+
return
48+
49+
new_result = [{"range": selection.to_range(), "newText": formatted_text}]
50+
51+
outcome.force_result(new_result)
52+
53+
54+
def select_text(text, selection):
55+
lines = text.splitlines(True)
56+
return "".join(lines[selection.start : selection.end])
57+
58+
59+
def format_text(text):
60+
return black.format_file_contents(text, line_length=88, fast=False)

setup.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[flake8]
2+
max-line-length = 88
3+
ignore = E203
4+
5+
[mypy]
6+
ignore_missing_imports = true

setup.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from setuptools import find_packages, setup
2+
3+
with open("README.md", "r") as f:
4+
long_description = f.read()
5+
6+
setup(
7+
name="pyls-black",
8+
version="0.1.0",
9+
description="Black plugin for the Python Language Server",
10+
long_description=long_description,
11+
long_description_content_type="text/markdown",
12+
url="https://github.com/rupert/pyls-black",
13+
author="Rupert Bedford",
14+
author_email="[email protected]",
15+
packages=find_packages(exclude=["tests"]),
16+
install_requires=["python-language-server", "black"],
17+
extras_require={"dev": ["isort", "flake8", "pytest"]},
18+
entry_points={"pyls": ["pyls_black = pyls_black.plugin"]},
19+
classifiers=(
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.6",
22+
"License :: OSI Approved :: MIT License",
23+
"Operating System :: OS Independent"
24+
),
25+
)

tests/formatted.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
a = "hello"
2+
b = 42

tests/invalid.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x = 1+2

tests/test_plugin.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from pathlib import Path
2+
from unittest.mock import Mock
3+
4+
import pytest
5+
from pyls.workspace import Document
6+
7+
from pyls_black.plugin import pyls_format_document, pyls_format_range
8+
9+
here = Path(__file__).parent
10+
11+
12+
@pytest.fixture
13+
def unformatted_document():
14+
path = here / "unformatted.txt"
15+
uri = f"file:/{path}"
16+
return Document(uri)
17+
18+
19+
@pytest.fixture
20+
def formatted_document():
21+
path = here / "formatted.txt"
22+
uri = f"file:/{path}"
23+
return Document(uri)
24+
25+
26+
@pytest.fixture
27+
def invalid_document():
28+
path = here / "invalid.txt"
29+
uri = f"file:/{path}"
30+
return Document(uri)
31+
32+
33+
def run(g, outcome):
34+
g.send(None)
35+
36+
with pytest.raises(StopIteration):
37+
g.send(outcome)
38+
39+
40+
def test_pyls_format_document(unformatted_document, formatted_document):
41+
mock = Mock()
42+
mock.get_result.return_value = None
43+
44+
g = pyls_format_document(unformatted_document)
45+
run(g, mock)
46+
47+
mock.force_result.assert_called_once_with(
48+
[
49+
{
50+
"range": {
51+
"start": {"line": 0, "character": 0},
52+
"end": {"line": 2, "character": 0},
53+
},
54+
"newText": formatted_document.source,
55+
}
56+
]
57+
)
58+
59+
60+
def test_pyls_format_document_with_result(unformatted_document):
61+
mock = Mock()
62+
mock.get_result.return_value = [
63+
{
64+
"range": {
65+
"start": {"line": 0, "character": 0},
66+
"end": {"line": 1, "character": 0},
67+
},
68+
"newText": "x = 1+2\n",
69+
}
70+
]
71+
72+
g = pyls_format_document(unformatted_document)
73+
run(g, mock)
74+
75+
mock.force_result.assert_called_once_with(
76+
[
77+
{
78+
"range": {
79+
"start": {"line": 0, "character": 0},
80+
"end": {"line": 2, "character": 0},
81+
},
82+
"newText": "x = 1 + 2\n",
83+
}
84+
]
85+
)
86+
87+
88+
def test_pyls_format_document_unchanged(formatted_document):
89+
mock = Mock()
90+
mock.get_result.return_value = None
91+
92+
g = pyls_format_document(formatted_document)
93+
run(g, mock)
94+
95+
mock.force_result.assert_not_called()
96+
97+
98+
def test_pyls_format_document_supresses_syntax_errors(invalid_document):
99+
mock = Mock()
100+
mock.get_result.return_value = None
101+
102+
g = pyls_format_document(invalid_document)
103+
run(g, mock)
104+
105+
mock.force_result.assert_not_called()
106+
107+
108+
@pytest.mark.parametrize(
109+
("start", "end", "expected"),
110+
[(0, 0, 'a = "hello"\n'), (1, 1, "b = 42\n"), (0, 1, 'a = "hello"\nb = 42\n')],
111+
)
112+
def test_pyls_format_range(unformatted_document, start, end, expected):
113+
range = {
114+
"start": {"line": start, "character": 0},
115+
"end": {"line": end, "character": 0},
116+
}
117+
118+
mock = Mock()
119+
mock.get_result.return_value = None
120+
121+
g = pyls_format_range(unformatted_document, range=range)
122+
run(g, mock)
123+
124+
mock.force_result.assert_called_once_with(
125+
[
126+
{
127+
"range": {
128+
"start": {"line": start, "character": 0},
129+
"end": {"line": end + 1, "character": 0},
130+
},
131+
"newText": expected,
132+
}
133+
]
134+
)
135+
136+
137+
def test_pyls_format_range_with_result(unformatted_document):
138+
range = {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}
139+
140+
mock = Mock()
141+
mock.get_result.return_value = [
142+
{
143+
"range": {
144+
"start": {"line": 0, "character": 0},
145+
"end": {"line": 2, "character": 0},
146+
},
147+
"newText": "x = 1+2\ny = 3+4\n",
148+
}
149+
]
150+
151+
g = pyls_format_range(unformatted_document, range=range)
152+
run(g, mock)
153+
154+
mock.force_result.assert_called_once_with(
155+
[
156+
{
157+
"range": {
158+
"start": {"line": 0, "character": 0},
159+
"end": {"line": 1, "character": 0},
160+
},
161+
"newText": "x = 1 + 2\n",
162+
}
163+
]
164+
)
165+
166+
167+
def test_pyls_format_range_unchanged(formatted_document):
168+
range = {"start": {"line": 0, "character": 0}, "end": {"line": 1, "character": 0}}
169+
170+
mock = Mock()
171+
mock.get_result.return_value = None
172+
173+
g = pyls_format_range(formatted_document, range=range)
174+
run(g, mock)
175+
176+
mock.force_result.assert_not_called()

0 commit comments

Comments
 (0)