Skip to content

Commit 572992d

Browse files
authored
Added continuous integration (#4)
This patch adds continuous integration based on GitHub workflows. While we are at it: * Added support for Python 3.6, 3.7 and 3.8 * Updated mypy and pylint to 0.790 and 2.6.0, respectively * Added isort, coverage and pydocstyle to checks * Introduced getters for ``path`` so that mypy correctly assumes it is a non-None
1 parent a39108e commit 572992d

10 files changed

Lines changed: 274 additions & 227 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Check-pull-request
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
Execute:
7+
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
python-version: [3.5, 3.6, 3.7, 3.8]
11+
12+
steps:
13+
- uses: actions/checkout@master
14+
15+
- name: Set up Python ${{ matrix.python-version }}
16+
uses: actions/setup-python@v2
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
20+
- name: Install dependencies
21+
run: |
22+
python3 -m pip install --upgrade pip
23+
pip3 install --upgrade coveralls
24+
pip3 install -e .[dev]
25+
26+
- name: Run checks
27+
run: ./precommit.py
28+
29+
- name: Upload Coverage
30+
run: coveralls
31+
env:
32+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
COVERALLS_FLAG_NAME: ${{ matrix.python-version }}
34+
COVERALLS_PARALLEL: true
35+
36+
Finish-Coveralls:
37+
name: Finish Coveralls
38+
needs: Execute
39+
runs-on: ubuntu-latest
40+
container: python:3-slim
41+
steps:
42+
- name: Finish Coveralls
43+
run: |
44+
pip3 install --upgrade coveralls
45+
coveralls --finish
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/check-push.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Check-push
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
Execute:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
python-version: [3.5, 3.6, 3.7, 3.8]
14+
15+
steps:
16+
- uses: actions/checkout@master
17+
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v2
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install dependencies
24+
run: |
25+
python3 -m pip install --upgrade pip
26+
pip3 install -e .[dev]
27+
pip3 install coveralls
28+
29+
- name: Run checks
30+
run: ./precommit.py
31+
32+
- name: Upload coverage to coveralls
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
run: coveralls

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ venv3
66
*.egg-info
77
.tox
88
dist/
9+
venv/
10+
.coverage

README.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
temppathlib
22
===========
33

4-
temppathlib provides wrappers around ``tempfile`` so that you can directly use them together with ``pathlib`` module.
4+
.. image:: https://github.com/Parquery/temppathlib/workflows/Check-push/badge.svg
5+
:target: https://github.com/Parquery/temppathlib/actions?query=workflow%3ACheck-push
6+
:alt: Check status
7+
8+
.. image:: https://coveralls.io/repos/github/Parquery/temppathlib/badge.svg?branch=master
9+
:target: https://coveralls.io/github/Parquery/temppathlib
10+
:alt: Test coverage
11+
12+
.. image:: https://badge.fury.io/py/temppathlib.svg
13+
:target: https://pypi.org/project/temppathlib/
14+
:alt: PyPI - version
15+
16+
.. image:: https://img.shields.io/pypi/pyversions/temppathlib.svg
17+
:target: https://pypi.org/project/temppathlib/
18+
:alt: PyPI - Python Version
19+
20+
Temppathlib provides wrappers around ``tempfile`` so that you can directly use them together with ``pathlib`` module.
521
We found it cumbersome to convert ``tempfile`` objects manually to ``pathlib.Path`` whenever we needed a temporary
622
file.
723

precommit.py

Lines changed: 76 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,202 +1,105 @@
11
#!/usr/bin/env python3
2-
"""
3-
Runs precommit checks on the repository.
4-
"""
2+
"""Run precommit checks on the repository."""
53
import argparse
6-
import concurrent.futures
7-
import hashlib
84
import os
95
import pathlib
6+
import re
107
import subprocess
118
import sys
12-
from typing import List, Union, Tuple # pylint: disable=unused-import
139

14-
import yapf.yapflib.yapf_api
15-
16-
17-
def compute_hash(text: str) -> str:
18-
"""
19-
:param text: to hash
20-
:return: hash digest
21-
"""
22-
md5 = hashlib.md5()
23-
md5.update(text.encode())
24-
return md5.hexdigest()
25-
26-
27-
class Hasher:
28-
"""
29-
Hashes the source code files and reports if they differed to one of the previous hashings.
30-
"""
31-
32-
def __init__(self, source_dir: pathlib.Path, hash_dir: pathlib.Path) -> None:
33-
self.source_dir = source_dir
34-
self.hash_dir = hash_dir
35-
36-
def __hash_dir(self, path: pathlib.Path) -> pathlib.Path:
37-
"""
38-
:param path: to a source file
39-
:return: path to the file holding the hash of the source text
40-
"""
41-
if self.source_dir not in path.parents:
42-
raise ValueError("Expected the path to be beneath the source directory {!r}, got: {!r}".format(
43-
str(self.source_dir), str(path)))
44-
45-
return self.hash_dir / path.relative_to(self.source_dir).parent / path.name
46-
47-
def hash_differs(self, path: pathlib.Path) -> bool:
48-
"""
49-
:param path: to the source file
50-
:return: True if the hash of the content differs to one of the previous hashings.
51-
"""
52-
hash_dir = self.__hash_dir(path=path)
53-
54-
if not hash_dir.exists():
55-
return True
56-
57-
prev_hashes = set([pth.name for pth in hash_dir.iterdir()])
58-
59-
new_hsh = compute_hash(text=path.read_text())
60-
61-
return not new_hsh in prev_hashes
62-
63-
def update_hash(self, path: pathlib.Path) -> None:
64-
"""
65-
Hashes the file content and stores it on disk.
66-
67-
:param path: to the source file
68-
:return:
69-
"""
70-
hash_dir = self.__hash_dir(path=path)
71-
hash_dir.mkdir(exist_ok=True, parents=True)
72-
73-
new_hsh = compute_hash(text=path.read_text())
74-
75-
pth = hash_dir / new_hsh
76-
pth.write_text('passed')
7710

11+
def main() -> int:
12+
"""Execute the main routine."""
13+
parser = argparse.ArgumentParser()
14+
parser.add_argument(
15+
"--overwrite",
16+
help="Overwrites the unformatted source files with the "
17+
"well-formatted code in place. If not set, "
18+
"an exception is raised if any of the files do not conform "
19+
"to the style guide.",
20+
action='store_true')
7821

79-
def check(path: pathlib.Path, py_dir: pathlib.Path, overwrite: bool) -> Union[None, str]:
80-
"""
81-
Runs all the checks on the given file.
22+
args = parser.parse_args()
8223

83-
:param path: to the source file
84-
:param py_dir: path to the source files
85-
:param overwrite: if True, overwrites the source file in place instead of reporting that it was not well-formatted.
86-
:return: None if all checks passed. Otherwise, an error message.
87-
"""
88-
style_config = py_dir / 'style.yapf'
24+
overwrite = bool(args.overwrite)
8925

90-
report = []
26+
repo_root = pathlib.Path(__file__).parent
9127

92-
# yapf
93-
if not overwrite:
94-
formatted, _, changed = yapf.yapflib.yapf_api.FormatFile(
95-
filename=str(path), style_config=str(style_config), print_diff=True)
28+
# yapf: disable
29+
source_files = (
30+
sorted((repo_root / "temppathlib").glob("**/*.py")) +
31+
sorted((repo_root / "tests").glob("**/*.py")))
32+
# yapf: enable
9633

97-
if changed:
98-
report.append("Failed to yapf {}:\n{}".format(path, formatted))
34+
if overwrite:
35+
print('Removing trailing whitespace...')
36+
for pth in source_files:
37+
pth.write_text(re.sub(r'[ \t]+$', '', pth.read_text(), flags=re.MULTILINE))
38+
39+
print("YAPF'ing...")
40+
yapf_targets = ["tests", "temppathlib", "setup.py", "precommit.py"]
41+
if overwrite:
42+
# yapf: disable
43+
subprocess.check_call(
44+
["yapf", "--in-place", "--style=style.yapf", "--recursive"] +
45+
yapf_targets,
46+
cwd=str(repo_root))
47+
# yapf: enable
9948
else:
100-
yapf.yapflib.yapf_api.FormatFile(filename=str(path), style_config=str(style_config), in_place=True)
101-
102-
# mypy
103-
env = os.environ.copy()
104-
env['PYTHONPATH'] = ":".join([py_dir.as_posix(), env.get("PYTHONPATH", "")])
105-
106-
proc = subprocess.Popen(
107-
['mypy', str(path), '--ignore-missing-imports'],
108-
stdout=subprocess.PIPE,
109-
stderr=subprocess.PIPE,
110-
env=env,
111-
universal_newlines=True)
112-
stdout, stderr = proc.communicate()
113-
if proc.returncode != 0:
114-
report.append("Failed to mypy {}:\nOutput:\n{}\n\nError:\n{}".format(path, stdout, stderr))
115-
116-
# pylint
117-
proc = subprocess.Popen(
118-
['pylint', str(path), '--rcfile={}'.format(py_dir / 'pylint.rc')],
119-
stdout=subprocess.PIPE,
120-
stderr=subprocess.PIPE,
121-
universal_newlines=True)
122-
123-
stdout, stderr = proc.communicate()
124-
if proc.returncode != 0:
125-
report.append("Failed to pylint {}:\nOutput:\n{}\n\nError:\n{}".format(path, stdout, stderr))
49+
# yapf: disable
50+
subprocess.check_call(
51+
["yapf", "--diff", "--style=style.yapf", "--recursive"] +
52+
yapf_targets,
53+
cwd=str(repo_root))
54+
# yapf: enable
12655

127-
if len(report) > 0:
128-
return "\n".join(report)
129-
130-
return None
56+
print("Mypy'ing...")
57+
subprocess.check_call(["mypy", "--strict", "temppathlib", "tests"], cwd=str(repo_root))
13158

59+
print("Isort'ing...")
60+
# yapf: disable
61+
isort_files = map(str, source_files)
62+
# yapf: enable
13263

133-
def main() -> int:
134-
""""
135-
Main routine
136-
"""
137-
# pylint: disable=too-many-locals
138-
parser = argparse.ArgumentParser()
139-
parser.add_argument(
140-
"--overwrite",
141-
help="Overwrites the unformatted source files with the well-formatted code in place. "
142-
"If not set, an exception is raised if any of the files do not conform to the style guide.",
143-
action='store_true')
64+
# yapf: disable
65+
subprocess.check_call(
66+
["isort", "--project", "temppathlib", '--line-width', '120'] +
67+
([] if overwrite else ['--check-only']) +
68+
[str(pth) for pth in source_files])
69+
# yapf: enable
14470

145-
parser.add_argument("--all", help="checks all the files even if they didn't change", action='store_true')
71+
print("Pydocstyle'ing...")
72+
subprocess.check_call(["pydocstyle", "temppathlib"], cwd=str(repo_root))
14673

147-
args = parser.parse_args()
74+
print("Pylint'ing...")
75+
subprocess.check_call(["pylint", "--rcfile=pylint.rc", "tests", "temppathlib"], cwd=str(repo_root))
14876

149-
overwrite = bool(args.overwrite)
150-
check_all = bool(args.all)
77+
print("Testing...")
78+
env = os.environ.copy()
79+
env['ICONTRACT_SLOW'] = 'true'
15180

152-
py_dir = pathlib.Path(__file__).parent
81+
# yapf: disable
82+
subprocess.check_call(
83+
["coverage", "run",
84+
"--source", "temppathlib",
85+
"-m", "unittest", "discover", "tests"],
86+
cwd=str(repo_root),
87+
env=env)
88+
# yapf: enable
15389

154-
hash_dir = py_dir / '.precommit_hashes'
155-
hash_dir.mkdir(exist_ok=True)
90+
subprocess.check_call(["coverage", "report"])
15691

157-
hasher = Hasher(source_dir=py_dir, hash_dir=hash_dir)
92+
print("Doctesting...")
93+
doctest_files = ([repo_root / "README.rst"] + sorted((repo_root / "temppathlib").glob("**/*.py")))
15894

159-
# yapf: disable
160-
pths = sorted(
161-
list(py_dir.glob("*.py")) +
162-
list((py_dir / 'tests').glob("*.py"))
163-
)
164-
# yapf: enable
95+
for pth in doctest_files:
96+
subprocess.check_call([sys.executable, "-m", "doctest", str(pth)])
16597

166-
# see which files changed:
167-
pending_pths = [] # type: List[pathlib.Path]
98+
print("Checking setup.py sdist ...")
99+
subprocess.check_call([sys.executable, "setup.py", "sdist"], cwd=str(repo_root))
168100

169-
if check_all:
170-
pending_pths = pths
171-
else:
172-
for pth in pths:
173-
if hasher.hash_differs(path=pth):
174-
pending_pths.append(pth)
175-
176-
print("There are {} file(s) that need to be individually checked...".format(len(pending_pths)))
177-
178-
success = True
179-
180-
futures_paths = [] # type: List[Tuple[concurrent.futures.Future, pathlib.Path]]
181-
with concurrent.futures.ThreadPoolExecutor() as executor:
182-
for pth in pending_pths:
183-
future = executor.submit(fn=check, path=pth, py_dir=py_dir, overwrite=overwrite)
184-
futures_paths.append((future, pth))
185-
186-
for future, pth in futures_paths:
187-
report = future.result()
188-
if report is None:
189-
print("Passed all checks: {}".format(pth))
190-
hasher.update_hash(path=pth)
191-
else:
192-
print("One or more checks failed for {}:\n{}".format(pth, report))
193-
success = False
194-
195-
success = subprocess.call(['python3', '-m', 'unittest', 'discover', (py_dir / 'tests').as_posix()]) == 0 and success
196-
197-
if not success:
198-
print("One or more checks failed.")
199-
return 1
101+
print("Checking with twine...")
102+
subprocess.check_call(["twine", "check", "dist/*"], cwd=str(repo_root))
200103

201104
return 0
202105

0 commit comments

Comments
 (0)