Skip to content

Commit 8dc73ce

Browse files
Merge branch 'main' into example-failed-unittest
2 parents f696db1 + 6ec707c commit 8dc73ce

File tree

8 files changed

+352
-289
lines changed

8 files changed

+352
-289
lines changed

Diff for: .github/workflows/python-ci.yml

+67-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
name: Python CI Example
22

3-
on: [push, pull_request]
3+
on: [push, pull_request, workflow_dispatch]
44

55
jobs:
66
build:
7-
87
runs-on: ubuntu-latest
9-
108
steps:
9+
1110
- name: Checkout repository
1211
uses: actions/checkout@v4
1312

1413
- name: Set up Python V3.11
15-
uses: actions/setup-python@v4
14+
uses: actions/setup-python@v5
1615
with:
1716
python-version: 3.11
1817

@@ -21,14 +20,75 @@ jobs:
2120
python -m pip install --upgrade pip
2221
pip install -r requirements.txt
2322
23+
- name: Set default environment variables
24+
run: |
25+
echo "mypy_warnings=INVALID" >> $GITHUB_ENV
26+
echo "pylint_score=INVALID" >> $GITHUB_ENV
27+
echo "coverage=INVALID" >> $GITHUB_ENV
28+
29+
- name: Run type checker
30+
run: |
31+
mypy source | tee mypy_output.txt
32+
MYPY_WARNINGS=$(grep -oP '\b\d+(?= errors?)|\bno issues found\b' mypy_output.txt | tail -n1 | sed 's/\bno issues found\b/0/')
33+
echo "mypy_warnings=${MYPY_WARNINGS}" >> $GITHUB_ENV
34+
continue-on-error: true
35+
36+
- name: Run static code analysis
37+
run: |
38+
pylint --output-format=parseable main.py source tests | tee pylint_output.txt
39+
PYLINT_SCORE=$(grep -oP 'Your code has been rated at \K[^/]+' pylint_output.txt)
40+
echo "pylint_score=${PYLINT_SCORE}" >> $GITHUB_ENV
41+
continue-on-error: true
42+
2443
- name: Run unittests
2544
run: |
2645
python -m unittest discover -v
27-
46+
2847
- name: Run coverage
2948
run: |
3049
coverage run -m unittest discover
3150
coverage report -m
51+
coverage json -o coverage.json
3252
33-
- name: Run static code analysis
34-
run: pylint main.py source tests
53+
COVERAGE_PERCENT=$(python -c "import json; print(round(json.load(open('coverage.json'))['totals']['percent_covered'], 2))")
54+
echo "coverage=${COVERAGE_PERCENT}" >> $GITHUB_ENV
55+
56+
- name: Create mypy warning badge
57+
if: always()
58+
uses: schneegans/dynamic-badges-action@v1.7.0
59+
with:
60+
auth: ${{ secrets.GIST_SECRET }}
61+
gistID: d506acc54dead9ca1d070488e813d253
62+
filename: mypy_warnings.json
63+
label: MyPy Warnings
64+
message: ${{ env.mypy_warnings }}
65+
valColorRange: ${{ env.mypy_warnings }}
66+
minColorRange: 1
67+
maxColorRange: 6
68+
invertColorRange: true
69+
70+
- name: Create pylint badge
71+
if: always()
72+
uses: schneegans/dynamic-badges-action@v1.7.0
73+
with:
74+
auth: ${{ secrets.GIST_SECRET }}
75+
gistID: d506acc54dead9ca1d070488e813d253
76+
filename: pylint-score.json
77+
label: Pylint Score
78+
message: ${{ env.pylint_score }}
79+
valColorRange: ${{ env.pylint_score }}
80+
minColorRange: 5
81+
maxColorRange: 9
82+
83+
- name: Create coverage badge
84+
if: always()
85+
uses: schneegans/dynamic-badges-action@v1.7.0
86+
with:
87+
auth: ${{ secrets.GIST_SECRET }}
88+
gistID: d506acc54dead9ca1d070488e813d253
89+
filename: coverage.json
90+
label: Coverage
91+
message: ${{ env.coverage }}%
92+
valColorRange: ${{ env.coverage }}%
93+
minColorRange: 50
94+
maxColorRange: 90

Diff for: README.md

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
1+
![ci build results](https://github.com/JuliusWiedemann/PythonCIExample/actions/workflows/python-ci.yml/badge.svg)
2+
![pylint-score](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/JuliusWiedemann/d506acc54dead9ca1d070488e813d253/raw/pylint-score.json)
3+
![mypy-warnings](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/JuliusWiedemann/d506acc54dead9ca1d070488e813d253/raw/mypy_warnings.json)
4+
![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/JuliusWiedemann/d506acc54dead9ca1d070488e813d253/raw/coverage.json)
5+
[![license](https://img.shields.io/badge/License-MIT-purple.svg)](LICENSE)
6+
17
# 🏗️ Python Continuous Integration - Example
28

3-
This repo shows a basic example of CI/CD in python with github actions
9+
This repository shows a basic example of CI/CD in python with [![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-2088FF?logo=github-actions&logoColor=white)](#). It can be used as a template to set up a new [![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=fff)](#) project.
10+
11+
Navigate to the branch [example-failed-unittest](https://github.com/JuliusWiedemann/PythonCIExample/tree/example-failed-unittest) or [example-failed-pylint](https://github.com/JuliusWiedemann/PythonCIExample/tree/example-failed-pylint) to view what a failed build will look like.
412

513
## Features
6-
- 🗒️ Static code analysis with pylint
7-
- 🧑‍🔬 Testing with python unittest module
8-
- ✏️ Code coverage with coverage module
14+
- Static code analysis with [pylint](https://www.pylint.org/).
15+
- Type checking with [mypy](https://www.mypy-lang.org/).
16+
- Testing with python [unittest](https://docs.python.org/3/library/unittest.html) module.
17+
- Code coverage with [coverage](https://pypi.org/project/coverage/) module.
18+
19+
## Usage
20+
- Download the source code and use it as a template for your new python project.
21+
- You need to follow [this](https://github.com/marketplace/actions/dynamic-badges) configuration to use the dynamic badges. Alternative: Just remove the "Create Badge" step from the yml file to disable the feature.
922

1023
## Output
24+
- This template uses [GitHub Actions](https://docs.github.com/en/actions) to run all the tools. It will generate dynamic badges with the [dynamic-badges](https://github.com/marketplace/actions/dynamic-badges) action.
25+
- Alternatively you can just look at the console output: [GitHub Actions Output](https://github.com/JuliusWiedemann/PythonCIExample/actions).
26+
27+
An example build can look like this:
1128
![](images/ci-report.png)
1229

1330
---

Diff for: mypy.ini

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[mypy]
2+
exclude = tests
3+
warn_return_any = True
4+
warn_unused_configs = True
5+
ignore_missing_imports = True
6+
7+
disallow_untyped_defs = True
8+
disallow_untyped_calls = True
9+
disallow_incomplete_defs = True

Diff for: requirements.txt

-17 Bytes
Binary file not shown.

Diff for: source/game.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,91 @@
66
# pylint: disable=E0401 # False positive
77
from pokemon import Pokemon
88

9-
def welcome():
9+
def welcome() -> None:
1010
"""
1111
Prints the welcome message
1212
"""
1313
print("Welcome to the pokemon game!")
1414

15-
def createNewPokemon():
15+
def createNewPokemon() -> Pokemon | bool:
1616
"""
1717
Lets the user create a new pokemon via text input
1818
Returns the new pokemon
1919
"""
20-
name = input("Please enter the name of your pokemon: ")
20+
name: str = input("Please enter the name of your pokemon: ")
2121
if name is None or name == "":
2222
print("Invalid name")
2323
return False
2424

2525
try:
26-
number = int(input("Please enter the number of your pokemon: "))
26+
number: int = int(input("Please enter the number of your pokemon: "))
2727
except ValueError:
2828
print("Invalid number")
2929
return False
3030

31-
pokeType = input("Please enter the type of your pokemon: ")
31+
pokeType: str = input("Please enter the type of your pokemon: ")
3232

33-
newPokemon = Pokemon(name, number, pokeType)
34-
print(f"Your created the following pokemon: {newPokemon}")
33+
newPokemon: Pokemon = Pokemon(name, number, pokeType)
34+
print(f"You created the following pokemon: {newPokemon}")
3535
return newPokemon
3636

37-
def attackPokemon(pokemonStorage):
37+
def attackPokemon(pokemonStorage: dict) -> bool:
3838
"""
3939
Allows the user to attack a pokemon
4040
"""
4141
if (len(pokemonStorage.keys())) < 2:
4242
print("You need at least 2 pokemon to fight.")
4343
return False
4444

45-
name1 = input("Enter the name of your pokemon: ")
46-
pokemon1 = getPokemon(pokemonStorage, name1)
45+
name1: str = input("Enter the name of your pokemon: ")
46+
pokemon1: Pokemon = getPokemon(pokemonStorage, name1)
4747
if not pokemon1:
4848
return False
4949

5050
name2 = input("Enter the name of the pokemon to attack: ")
51-
pokemon2 = getPokemon(pokemonStorage, name2)
51+
pokemon2: Pokemon = getPokemon(pokemonStorage, name2)
5252
if not pokemon2:
5353
return False
5454

5555
pokemon1.attack(pokemon2)
5656
return True
5757

58-
def viewPokemonStats(pokemonStorage):
58+
def viewPokemonStats(pokemonStorage: dict) -> None:
5959
"""
6060
Allows the user to view stats of one pokemon he created
6161
"""
6262
name = input("Enter the name of your pokemon: ")
6363

64-
pokemon = getPokemon(pokemonStorage, name)
64+
pokemon: Pokemon = getPokemon(pokemonStorage, name)
6565
if pokemon:
6666
print(pokemon)
6767

68-
def getPokemon(pokemonStorage, name):
68+
def getPokemon(pokemonStorage: dict, name: str) -> Pokemon | bool:
6969
"""
7070
Searches a pokemon by name from the pokemon storage and returns it
7171
Returns false if pokemon does not exist
7272
"""
73-
pokemon = pokemonStorage.get(name)
73+
pokemon: Pokemon = pokemonStorage.get(name)
7474
if pokemon is None:
7575
print(f"Sorry. The pokemon {name} does not exist.")
7676
return False
7777
return pokemon
7878

79-
def main():
79+
def main() -> None:
8080
"""
8181
Creates while loop which lets the user create and fight pokemon via text input
8282
"""
83-
game = True
84-
pokemonStorage = {}
83+
game: bool = True
84+
pokemonStorage: dict = {}
8585

8686
welcome()
8787
while game:
8888
print("\n\n--------------------------------\n\n")
8989

90-
choice = input("What to you want to do?\n1: Create new pokemon\n2: Attack a pokemon\n3: View Stats\nQ: Quit Game\n")
90+
choice: str = input("What to you want to do?\n1: Create new pokemon\n2: Attack a pokemon\n3: View Stats\nQ: Quit Game\n")
9191

9292
if choice == "1":
93-
newPokemon = createNewPokemon()
93+
newPokemon: Pokemon = createNewPokemon()
9494
if newPokemon:
9595
pokemonStorage[newPokemon.getName()] = newPokemon
9696
elif choice == "2":

0 commit comments

Comments
 (0)