8000 Merge pull request #14 from suaaa7/13_python_ci · suaaa7/samplecode-for-qiita@80d0294 · GitHub
[go: up one dir, main page]

Skip to content

Commit 80d0294

Browse files
authored
Merge pull request #14 from suaaa7/13_python_ci
Python CI with flake8 and mypy
2 parents 5b05932 + 2e30521 commit 80d0294

13 files changed

+224
-0
lines changed

.github/workflows/python_ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Python CI
2+
on: [pull_request]
3+
4+
jobs:
5+
test:
6+
runs-on: ubuntu-18.04
7+
steps:
8+
- name: Checkout
9+
uses: actions/checkout@v2
10+
- name: Set up Python 3.8
11+
uses: actions/setup-python@v2
12+
with:
13+
python-version: '3.8'
14+
architecture: 'x64'
15+
- name: Install Library
16+
run: |
17+
cd python_ci
18+
python3 -m pip install --upgrade pip
19+
pip install -r requirements-dev.txt
20+
pip install -r requirements.txt
21+
- name: Run flake8
22+
run: |
23+
cd python_ci
24+
make flake8
25+
- name: Run mypy
26+
run: |
27+
cd python_ci
28+
make mypy
29+
- name: Run unittest
30+
run: |
31+
cd python_ci
32+
make test

python_ci/Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM python:3.8-slim
2+
3+
COPY requirements.txt .
4+
COPY requirements-dev.txt .
5+
6 EDBE +
RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
7+
&& pip install --no-cache-dir -r requirements.txt \
8+
&& pip install --no-cache-dir -r requirements-dev.txt

python_ci/Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
image = python-ci
2+
tag = 0.1.0
3+
curdir := `pwd`
4+
5+
.PHONY: dbuild
6+
dbuild:
7+
docker build -t $(image):$(tag) .
8+
9+
.PHONY: drun
10+
drun:
11+
docker run --rm -it -v $(curdir):/opt -w /opt $(image):$(tag) bash
12+
13+
.PHONY: drmi
14+
drmi:
15+
docker rmi -f $(image):$(tag)
16+
17+
.PHONY: flake8
18+
flake8:
19+
flake8 --config=config/tox.ini src
20+
21+
.PHONY: mypy
22+
mypy:
23+
mypy --config=config/mypy.ini src
24+
25+
.PHONY: test
26+
test:
27+
python3 -m unittest discover --verbose src

python_ci/config/mypy.ini

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[mypy]
2+
python_version = 3.8
3+
disallow_untyped_defs = True
4+
ignore_missing_imports = True
5+
warn_redundant_casts = True
6+
no_implicit_optional = True
7+
8+
[mypy-src.tests.*]
9+
disallow_untyped_defs = False

python_ci/config/tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
max-line-length = 79
3+
per-file-ignores =
4+
src/tests/*:E501

python_ci/requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
flake8==3.8.3
2+
mypy==0.782

python_ci/requirements.txt

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

python_ci/src/__init__.py

Whitespace-only changes.

python_ci/src/config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass(frozen=True)
5+
class BaseConfig:
6+
version: str
7+
model_path: str
8+
model_s3_bucket: str
9+
model_file_name: str
10+
11+
def generate_model_path(self) -> str:
12+
return "{}/{}/{}".format(
13+
self.model_path,
14+
self.version,
15+
self.model_file_name
16+
)
17+
18+
def generate_model_s3_path(self) -> str:
19+
return "s3://{}/{}/{}".format(
20+
self.model_s3_bucket,
21+
self.version,
22+
self.model_file_name
23+
)
24+
25+
26+
@dataclass(frozen=True)
27+
class Train:
28+
batch_size: int = 16
29+
epoch: int = 10
30+
31+
32+
@dataclass(frozen=True)
33+
class Test:
34+
batch_size: int = 16
35+
36+
37+
@dataclass(frozen=True)
38+
class Config(BaseConfig):
39+
version: str = "v1.0.0"
40+
model_path: str = "/ops/models"
41+
model_s3_bucket: str = "models"
42+
model_file_name: str = "model.pth"
43+
train: Train = Train()
44+
test: Test = Test()

python_ci/src/sample.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import requests
2+
3+
4+
class Sample:
5+
def fetch_json(self, url: str) -> dict:
6+
response = self._get_response(url)
7+
return response.json()
8+
9+
def _get_response(self, url: str) -> requests.models.Response:
10+
return requests.get(url)

python_ci/src/tests/__init__.py

Whitespace-only changes.

python_ci/src/tests/test_config.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from dataclasses import FrozenInstanceError
2+
from unittest import TestCase, main
3+
4+
from src.config import BaseConfig, Config
5+
6+
7+
class TestConfig(TestCase):
8+
def test_generate_path(self):
9+
config = BaseConfig(
10+
version="v0.0.0",
11+
model_path="/opt/models",
12+
model_s3_bucket="models",
13+
model_file_name="model.pth"
14+
)
15+
16+
expected_model_path = "/opt/models/v0.0.0/model.pth"
17+
expected_model_s3_path = "s3://models/v0.0.0/model.pth"
18+
19+
self.assertEqual(config.generate_model_path(), expected_model_path)
20+
self.assertEqual(config.generate_model_s3_path(), expected_model_s3_path)
21+
22+
def test_config_can_call_method(self):
23+
config = Config()
24+
config.generate_model_path()
25+
config.generate_model_s3_path()
26+
27+
def test_config_is_immutable(self):
28+
config = Config()
29+
30+
with self.assertRaises(FrozenInstanceError):
31+
config.version = "v0.0.0"
32+
33+
with self.assertRaises(FrozenInstanceError):
34+
config.train.epoch = 10000
35+
36+
with self.assertRaises(FrozenInstanceError):
37+
config.test.batch_siz = 64
38+
39+
40+
if __name__ == '__main__':
41+
main()

python_ci/src/tests/test_sample.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from unittest import TestCase, main
2+
from unittest.mock import MagicMock, call, patch
3+
4+
from src.sample import Sample
5+
6+
7+
class TestSample(TestCase):
8+
def mocked_requests_get(*args, **kwargs):
9+
class MockResponse:
10+
def __init__(self, json_data, status_code):
11+
self.json_data = json_data
12+
self.status_code = status_code
13+
14+
def json(self):
15+
return self.json_data
16+
17+
if args[0] == 'http://example.com/test.json':
18+
return MockResponse({'key1': 'value1'}, 200)
19+
elif args[0] == 'http://example.com/another_test.json':
20+
return MockResponse({'key2': 'value2'}, 200)
21+
22+
return MockResponse(None, 404)
23+
24+
@patch('requests.get', side_effect=mocked_requests_get)
25+
def test_fetch_json(self, mock_get: MagicMock):
26+
sample = Sample()
27+
28+
json_data = sample.fetch_json('http://example.com/test.json')
29+
self.assertEqual(json_data, {'key1': 'value1'})
30+
json_data = sample.fetch_json('http://example.com/another_test.json')
31+
self.assertEqual(json_data, {'key2': 'value2'})
32+
json_data = sample.fetch_json('http://no_example.com/test.json')
33+
self.assertIsNone(json_data)
34+
35+
self.assertIn(
36+
call('http://example.com/test.json'), mock_get.call_args_list
37+
)
38+
self.assertIn(
39+
call('http://example.com/another_test.json'), mock_get.call_args_list
40+
)
41+
42+
self.assertEqual(len(mock_get.call_args_list), 3)
43+
44+
45+
if __name__ == '__main__':
46+
main()

0 commit comments

Comments
 (0)
0