Update Lab 4
This commit is contained in:
parent
28b3da235c
commit
fb355f26e8
|
@ -1 +1,116 @@
|
|||
敬请期待
|
||||
# Lab 4:CI/CD流水线搭建(10分)
|
||||
|
||||
**截止日期:2022.11.02 11:59am**
|
||||
|
||||
## 实践目的
|
||||
|
||||
* 了解使用GitHub Action搭建CI/CD流水线的基本方法。
|
||||
* 以Python为例,了解典型的软件项目应当具有的基本开发工具、框架和流程。
|
||||
|
||||
## 初始文件
|
||||
|
||||
[Lab4/](Lab4/)文件夹中是一个功能极简的用Python实现图数据结构的包`pygraph`,该项目目前采用[Poetry](https://python-poetry.org/)进行依赖管理,[pytest](https://docs.pytest.org/en/7.1.x/)作为测试框架,文件结构如下:
|
||||
|
||||
* `pygraph.py`实现图数据结构,包含一个类和一些简单函数,均未实现,且结构混乱。
|
||||
* `pyproject.toml`是Poetry使用的依赖配置文件。
|
||||
* `tests/`文件夹包含若干测试。
|
||||
* `pyproject.toml`是`pytest`的配置文件。
|
||||
|
||||
Lab 4需要为这个项目配置开发环境,补全未实现的代码,并配置一个包含所有常见阶段的CI/CD流水线。
|
||||
|
||||
## 实践流程
|
||||
|
||||
1. 请在[OSS-Dev-Course-PKU](https://github.com/OSS-Dev-Course-PKU)中创建一个**私有**GitHub仓库,名为`2022Fall-{学号}-Lab4`,为老师和助教添加访问权限,并将[Lab4/](Lab4/)文件夹中的所有文件push到仓库内(注意不是push文件夹本身)。
|
||||
|
||||
> 不要设置为公开仓库或者为其他同学添加访问权限,否则此Lab评分作废;若误操作,请联系助教删除仓库
|
||||
|
||||
2. 将新创建的仓库clone到本地,配置Python开发环境,安装Poetry,使用Poetry配置虚拟环境,并安装所有依赖。
|
||||
|
||||
```shell script
|
||||
# 若Python和Poetry配置正确,用如下两行命令即可配置虚拟环境并安装所有依赖
|
||||
poetry shell
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. 使用[black](https://black.readthedocs.io/en/stable/)对代码进行重新格式化。
|
||||
|
||||
```shell script
|
||||
black .
|
||||
```
|
||||
|
||||
4. 使用[pre-commit](https://pre-commit.com/)工具为项目添加Pre-Commit Hook,使得git能够在commit前,调用black对代码进行自动格式化。这需要涉及到编写一个`.pre-commit-config.yaml`文件,并运行`pre-commit install`命令。
|
||||
|
||||
> 为项目配置统一、简洁的代码风格,是为了降低其他人阅读代码时的认知负荷,方便团队协作。这一流程可以利用现有工具高度自动化,不会为开发带来任何额外负担。
|
||||
|
||||
5. 修改`pygraph.py`文件,实现其中所有功能,保证运行如下命令可以通过所有测试,并生成测试覆盖率报告。
|
||||
|
||||
```shell script
|
||||
pytest -r P --cov=pygraph
|
||||
```
|
||||
|
||||
> 这里采用的开发范式是[测试驱动开发](https://en.wikipedia.org/wiki/Test-driven_development)(Test-Driven Development,TDD),先通过编写测试用例精准描述需求,再在测试用例的基础上实现代码。这一开发方式可以“倒逼”程序员思考代码实现的简洁性、灵活性和可测试性,有助于提升项目代码质量,在敏捷开发中经常使用。
|
||||
|
||||
> 由于本Lab的核心要点并不在于代码实现,因此这里刻意使用了非常简单的代码。不过需要注意,Lab里涉及到的代码实践可适用于,且往往对于长期维护的大型软件项目不可或缺。我们强烈鼓励同学们在自己的其他项目中借鉴运用相关实践(对于各种技术栈,其内核都是相通的),提升整体的工程水平。
|
||||
|
||||
6. 使用[pdoc3](https://pdoc3.github.io/pdoc/)为`pygraph`自动生成API文档,可以在`html/`文件夹中查看生成的API文档。
|
||||
|
||||
```shell script
|
||||
pdoc --html pygraph
|
||||
```
|
||||
|
||||
7. 使用GitHub Action配置五阶段CI/CD流水线,包含如下步骤:
|
||||
|
||||
* 初始化Python环境,安装Poetry;
|
||||
* 使用Poetry自动安装所有依赖;
|
||||
* 使用black检测代码是否存在格式问题;
|
||||
* 使用pytest运行单元测试;
|
||||
* 使用pdoc3生成API文档,并将API文档部署到仓库中的`gh-page`分支。
|
||||
|
||||
> 对于公有仓库,可以配置GitHub读取`gh-page`分支,在github.io直接提供相应的公开可访问的网页,这样便实现了API文档的持续部署(对于软件包本身的持续部署,我们留到Lab 5再实现)。
|
||||
>
|
||||
> 不过非常遗憾的是,私有仓库的GitHub Page是收费功能。如果同学们以后创建自己的项目,可以尝试用同样的方法自动部署各种网页,确认实际效果。
|
||||
|
||||
8. 将所有更改体现在GitHub仓库中。
|
||||
|
||||
## 提交前检查
|
||||
|
||||
在提交前,请确保在一台安装了Python和Poetry的机器上,能够依次运行如下命令并正常输出结果
|
||||
|
||||
```shell script
|
||||
poetry shell # activate a working virtual environment
|
||||
poetry install # install all dependencies
|
||||
pre-commit install # install pre-commit hooks
|
||||
black . # lint all Python code
|
||||
pytest -r P --cov=pygraph # run all tests with test stdout and coverage report
|
||||
pdoc --html pygraph # build API documentation and deploy to html/
|
||||
```
|
||||
|
||||
> 对于所有需要给别人使用的代码,确保其他人能够轻松地按照开发文档,配置能够运行你的代码的标准化的开发环境,是至关重要的。Python生态里的大量工具链都是为了能够做到这一点。
|
||||
|
||||
此外,请确保你的GitHub仓库里,GitHub Action包含了所有要求的流水线阶段,且能不报错正常执行(有小绿勾)。
|
||||
|
||||
> 助教会人工检查流水线配置是否正确。需要尤其注意的是,`black`在默认配置下,即使存在代码格式问题,也只会重新格式化而不会报错,这对于CI流水线中的检查是不合适的(CI环境下重新格式化了也没用)。因此,需要通过相应配置选项让`black`检查是否存在风格错误。
|
||||
|
||||
## 评分标准
|
||||
|
||||
- (3分)所有测试能够通过。
|
||||
- (2分)配置了能够自动格式化代码的Pre-Commit Hook。
|
||||
- (5分)配置了至少包含如下五个阶段的GitHub Action流水线:初始化Python环境、安装依赖、代码风格检查、运行单元测试、部署API文档。
|
||||
|
||||
## 提交方式
|
||||
|
||||
Lab 4无需特意提交任何内容,助教会在DDL后检查[OSS-Dev-Course-PKU](https://github.com/OSS-Dev-Course-PKU)中检查相应仓库,做出最终评分。
|
||||
|
||||
## 参考资料
|
||||
|
||||
1. [Poetry文档](https://python-poetry.org/docs/)
|
||||
2. [pytest文档](https://docs.pytest.org/en/7.1.x/getting-started.html)
|
||||
3. [pytest测试里用到的fixture功能](https://docs.pytest.org/en/6.2.x/fixture.html)
|
||||
4. [black文档](https://black.readthedocs.io/en/stable/)
|
||||
5. [pre-commit文档](https://pre-commit.com/)
|
||||
6. [pdoc3文档](https://pdoc3.github.io/pdoc/)
|
||||
7. [GitHub Action文档](https://docs.github.com/en/actions)
|
||||
8. 可能有用的GitHub Action插件:
|
||||
1. https://black.readthedocs.io/en/stable/integrations/github_actions.html
|
||||
2. https://github.com/actions/setup-python
|
||||
3. https://github.com/marketplace/actions/deploy-to-github-pages
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
html/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# 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/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
|
@ -0,0 +1,57 @@
|
|||
from typing import Tuple, Union, Iterable
|
||||
Node = Union[str, int]
|
||||
Edge = Tuple[Node, Node]
|
||||
|
||||
|
||||
|
||||
class Graph(object):
|
||||
"""Graph data structure, undirected by default."""
|
||||
def __init__(self, edges: Iterable[Edge] = [], directed: bool = False):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def has_node(self, node: Node):
|
||||
"""Whether a node is in graph"""
|
||||
raise NotImplementedError()
|
||||
def has_edge(self, edge: Edge):
|
||||
|
||||
|
||||
|
||||
"""Whether an edge is in graph"""
|
||||
raise NotImplementedError()
|
||||
def add_node(self, node: Node):
|
||||
"""Add a node"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def add_edge(self, edge: Edge):
|
||||
|
||||
|
||||
"""Add an edge (node1, node2). For directed graph, node1 -> node2"""
|
||||
raise NotImplementedError()
|
||||
def remove_node(self, node: Node):
|
||||
"""Remove all references to node"""
|
||||
|
||||
|
||||
raise NotImplementedError()
|
||||
def remove_edge(self, edge: Edge):
|
||||
|
||||
|
||||
"""Remove an edge from graph"""
|
||||
raise NotImplementedError()
|
||||
def indegree(self, node: Node) -> int:
|
||||
"""Compute indegree for a node"""
|
||||
raise NotImplementedError()
|
||||
def outdegree(self, node: Node) -> int:
|
||||
|
||||
|
||||
"""Compute outdegree for a node"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
raise NotImplementedError()
|
||||
def __repr__(self):
|
||||
|
||||
|
||||
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,15 @@
|
|||
[tool.poetry]
|
||||
name = "pygraph"
|
||||
version = "0.1.0"
|
||||
description = "Simple Python Graph Library"
|
||||
authors = []
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2"
|
||||
pytest-cov = "^3.0.0"
|
||||
black = "*"
|
||||
pre-commit = "*"
|
||||
pdoc3 = "0.10.0"
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
python_files = pygraph.py tests/*.py
|
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from pygraph import Graph
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def trivial():
|
||||
return Graph([])
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def small_directed():
|
||||
return Graph([("a", "b"), ("b", "c"), ("b", "d"), ("c", "d")], directed=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def small_undirected():
|
||||
return Graph([("a", "b"), ("b", "c"), ("b", "d"), ("c", "d")], directed=False)
|
|
@ -0,0 +1,89 @@
|
|||
import pytest
|
||||
|
||||
from pprint import pprint
|
||||
from pygraph import Graph
|
||||
|
||||
|
||||
def test_init_graph():
|
||||
graph = Graph(edges=[(1, 2)], directed=True)
|
||||
assert graph.has_node(1)
|
||||
assert graph.has_node(2)
|
||||
assert graph.has_edge((1, 2))
|
||||
pprint(graph)
|
||||
|
||||
graph = Graph(edges=[(1, 2)], directed=False)
|
||||
assert graph.has_node(1)
|
||||
assert graph.has_node(2)
|
||||
assert graph.has_edge((1, 2))
|
||||
assert graph.has_edge((2, 1))
|
||||
pprint(graph)
|
||||
|
||||
|
||||
def test_add_node(trivial: Graph):
|
||||
trivial.add_node(1)
|
||||
assert trivial.has_node(1)
|
||||
pprint(trivial)
|
||||
|
||||
|
||||
def test_add_edge(trivial: Graph, small_directed: Graph):
|
||||
trivial.add_edge((1, 2))
|
||||
trivial.add_edge((3, 4))
|
||||
for i in range(1, 5):
|
||||
assert trivial.has_node(i)
|
||||
assert trivial.has_edge((1, 2))
|
||||
assert trivial.has_edge((3, 4))
|
||||
pprint(trivial)
|
||||
|
||||
small_directed.add_edge(("d", "b"))
|
||||
assert small_directed.has_edge(("d", "b"))
|
||||
assert small_directed.has_edge(("b", "d"))
|
||||
pprint(small_directed)
|
||||
|
||||
|
||||
def test_remove_node(trivial: Graph, small_directed: Graph, small_undirected: Graph):
|
||||
with pytest.raises(ValueError):
|
||||
trivial.remove_node("a")
|
||||
|
||||
small_directed.remove_node("b")
|
||||
assert not small_directed.has_node("b")
|
||||
assert not small_directed.has_edge(("a", "b"))
|
||||
assert not small_directed.has_edge(("b", "c"))
|
||||
assert not small_directed.has_edge(("b", "d"))
|
||||
assert small_directed.has_node("a")
|
||||
assert small_directed.has_node("c")
|
||||
assert small_directed.has_node("d")
|
||||
assert small_directed.has_edge(("c", "d"))
|
||||
pprint(small_directed)
|
||||
|
||||
small_undirected.remove_node("b")
|
||||
assert not small_undirected.has_node("b")
|
||||
assert not small_undirected.has_edge(("a", "b"))
|
||||
assert not small_undirected.has_edge(("b", "a"))
|
||||
assert not small_undirected.has_edge(("b", "c"))
|
||||
assert not small_undirected.has_edge(("c", "b"))
|
||||
assert not small_undirected.has_edge(("b", "d"))
|
||||
assert not small_undirected.has_edge(("d", "b"))
|
||||
assert small_undirected.has_node("a")
|
||||
assert small_undirected.has_node("c")
|
||||
assert small_undirected.has_node("d")
|
||||
assert small_undirected.has_edge(("c", "d"))
|
||||
assert small_undirected.has_edge(("d", "c"))
|
||||
pprint(small_undirected)
|
||||
|
||||
|
||||
def test_remove_edge(trivial: Graph, small_directed: Graph, small_undirected: Graph):
|
||||
with pytest.raises(ValueError):
|
||||
trivial.remove_edge(("a", "b"))
|
||||
|
||||
small_directed.add_edge(("c", "b"))
|
||||
small_directed.remove_edge(("b", "c"))
|
||||
assert small_directed.has_node("b") and small_directed.has_node("c")
|
||||
assert not small_directed.has_edge(("b", "c"))
|
||||
assert small_directed.has_edge(("c", "b"))
|
||||
pprint(small_directed)
|
||||
|
||||
small_undirected.remove_edge(("b", "c"))
|
||||
assert small_undirected.has_node("b") and small_directed.has_node("c")
|
||||
assert not small_undirected.has_edge(("b", "c"))
|
||||
assert not small_undirected.has_edge(("c", "b"))
|
||||
pprint(small_undirected)
|
|
@ -0,0 +1,26 @@
|
|||
from pprint import pprint
|
||||
from pygraph import Graph
|
||||
|
||||
|
||||
def test_indegree(small_directed: Graph, small_undirected: Graph):
|
||||
assert small_undirected.indegree("a") == 1
|
||||
assert small_undirected.indegree("b") == 3
|
||||
assert small_undirected.indegree("c") == 2
|
||||
assert small_undirected.indegree("d") == 2
|
||||
|
||||
assert small_directed.indegree("a") == 0
|
||||
assert small_directed.indegree("b") == 1
|
||||
assert small_directed.indegree("c") == 1
|
||||
assert small_directed.indegree("d") == 2
|
||||
|
||||
|
||||
def test_outdegree(small_directed: Graph, small_undirected: Graph):
|
||||
assert small_undirected.outdegree("a") == 1
|
||||
assert small_undirected.outdegree("b") == 3
|
||||
assert small_undirected.outdegree("c") == 2
|
||||
assert small_undirected.outdegree("d") == 2
|
||||
|
||||
assert small_directed.outdegree("a") == 1
|
||||
assert small_directed.outdegree("b") == 2
|
||||
assert small_directed.outdegree("c") == 1
|
||||
assert small_directed.outdegree("d") == 0
|
Loading…
Reference in New Issue