Skip to content

Commit fb9b3d9

Browse files
committed
Initial commit
0 parents  commit fb9b3d9

12 files changed

+425
-0
lines changed

.editorconfig

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = tab
6+
indent_size = 4
7+
insert_final_newline = true
8+
9+
[*.{yml,yaml}]
10+
indent_size = 2
11+
indent_style = space

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__
2+
*.pyc
3+
*.pyo
4+
/*.egg-info
5+
/build
6+
/dist
7+
/.eggs

Code_Of_Conduct.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No codes of conduct!

ReadMe.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
setup-python GitHub Action [![`Unlicense`d work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/)
2+
==========================
3+
4+
Setting-up python without being directly implemented in Node.js shit.
5+
6+
Hardens the image a bit, then bootstraps packaging, then setups packages neeeded for testing.

UNLICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <https://unlicense.org/>

action.sh

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env bash
2+
3+
set -e;
4+
5+
if [[ -z "${ACTIONS_RUNTIME_URL}" ]]; then
6+
echo "::error::ACTIONS_RUNTIME_URL is missing. Uploading artifacts won't work without it. See https://github.com/KOLANICH-GHActions/passthrough-restricted-actions-vars and https://github.com/KOLANICH-GHActions/node_based_cmd_action_template";
7+
exit 1;
8+
fi;
9+
10+
if [[ -z "${ACTIONS_RUNTIME_TOKEN}" ]]; then
11+
echo "::error::ACTIONS_RUNTIME_TOKEN is missing. Uploading artifacts won't work without it. See https://github.com/KOLANICH-GHActions/passthrough-restricted-actions-vars and https://github.com/KOLANICH-GHActions/node_based_cmd_action_template";
12+
exit 1;
13+
fi;
14+
15+
NEED_PYTEST=$1;
16+
SHOULD_ISOLATE_TESTING=$2;
17+
18+
THIS_SCRIPT_DIR=`dirname "${BASH_SOURCE[0]}"`; # /home/runner/work/_actions/KOLANICH-GHActions/typical-python-workflow/master
19+
echo "This script is $THIS_SCRIPT_DIR";
20+
THIS_SCRIPT_DIR=`realpath "${THIS_SCRIPT_DIR}"`;
21+
echo "This script is $THIS_SCRIPT_DIR";
22+
ACTIONS_DIR=`realpath "$THIS_SCRIPT_DIR/../../.."`;
23+
24+
ISOLATE="${THIS_SCRIPT_DIR}/isolate.sh";
25+
26+
AUTHOR_NAMESPACE=KOLANICH-GHActions;
27+
28+
SETUP_ACTION_REPO=$AUTHOR_NAMESPACE/setup-python;
29+
GIT_PIP_ACTION_REPO=$AUTHOR_NAMESPACE/git-pip;
30+
APT_ACTION_REPO=$AUTHOR_NAMESPACE/apt;
31+
COVERAGEPY_ACTION_REPO=$AUTHOR_NAMESPACE/coveragepyReport;
32+
CHECKOUT_ACTION_REPO=$AUTHOR_NAMESPACE/checkout;
33+
34+
SETUP_ACTION_DIR=$ACTIONS_DIR/$SETUP_ACTION_REPO/master;
35+
GIT_PIP_ACTION_DIR=$ACTIONS_DIR/$GIT_PIP_ACTION_REPO/master;
36+
APT_ACTION_DIR=$ACTIONS_DIR/$APT_ACTION_REPO/master;
37+
COVERAGEPY_ACTION_DIR=$ACTIONS_DIR/$COVERAGEPY_ACTION_REPO/master;
38+
CHECKOUT_ACTION_DIR=$ACTIONS_DIR/$CHECKOUT_ACTION_REPO/master;
39+
40+
if [ -d "$CHECKOUT_ACTION_DIR" ]; then
41+
:
42+
else
43+
$ISOLATE git clone --depth=1 https://github.com/$CHECKOUT_ACTION_REPO $CHECKOUT_ACTION_DIR;
44+
fi;
45+
46+
if [ -d "$SETUP_ACTION_DIR" ]; then
47+
:
48+
else
49+
$ISOLATE bash "$CHECKOUT_ACTION_DIR/action.sh" "$SETUP_ACTION_REPO" "" "$SETUP_ACTION_DIR" 1 0;
50+
fi;
51+
52+
if [ -d "$GIT_PIP_ACTION_DIR" ]; then
53+
:
54+
else
55+
$ISOLATE bash "$CHECKOUT_ACTION_DIR/action.sh" "$GIT_PIP_ACTION_REPO" "" "$GIT_PIP_ACTION_DIR" 1 0;
56+
fi;
57+
58+
if [ -d "$APT_ACTION_DIR" ]; then
59+
:
60+
else
61+
$ISOLATE bash "$CHECKOUT_ACTION_DIR/action.sh" "$APT_ACTION_REPO" "" "$APT_ACTION_DIR" 1 0;
62+
fi;
63+
64+
bash $SETUP_ACTION_DIR/action.sh $NEED_PYTEST;
65+
66+
67+
$ISOLATE bash "$CHECKOUT_ACTION_DIR/action.sh" "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$GITHUB_WORKSPACE" 1 1;
68+
69+
BEFORE_DEPS_COMMANDS_FILE="$GITHUB_WORKSPACE/.ci/beforeDeps.sh";
70+
if [ -f "$BEFORE_DEPS_COMMANDS_FILE" ]; then
71+
echo "##[group] Running before deps commands";
72+
. $BEFORE_DEPS_COMMANDS_FILE ;
73+
echo "##[endgroup]";
74+
fi;
75+
76+
echo "##[group] Installing dependencies";
77+
bash $APT_ACTION_DIR/action.sh $GITHUB_WORKSPACE/.ci/aptPackagesToInstall.txt;
78+
bash $GIT_PIP_ACTION_DIR/action.sh $GITHUB_WORKSPACE/.ci/pythonPackagesToInstallFromGit.txt;
79+
echo "##[endgroup]";
80+
81+
echo "##[group] Getting package name";
82+
PACKAGE_NAME=`$ISOLATE python3 $THIS_SCRIPT_DIR/getPackageName.py $GITHUB_WORKSPACE`;
83+
echo "##[endgroup]";
84+
85+
cd "$GITHUB_WORKSPACE";
86+
87+
BEFORE_BUILD_COMMANDS_FILE="$GITHUB_WORKSPACE/.ci/beforeBuild.sh";
88+
if [ -f "$BEFORE_BUILD_COMMANDS_FILE" ]; then
89+
echo "##[group] Running before build commands";
90+
. $BEFORE_BUILD_COMMANDS_FILE;
91+
echo "##[endgroup]";
92+
fi;
93+
94+
echo "##[group] Building the main package";
95+
$ISOLATE python3 -m build -xnw .;
96+
PACKAGE_FILE_NAME=$PACKAGE_NAME-0.CI-py3-none-any.whl;
97+
PACKAGE_FILE_PATH=./dist/$PACKAGE_FILE_NAME;
98+
mv ./dist/*.whl $PACKAGE_FILE_PATH;
99+
echo "##[endgroup]";
100+
101+
echo "##[group] Installing the main package";
102+
$ISOLATE sudo pip3 install --upgrade $PACKAGE_FILE_PATH;
103+
#$ISOLATE sudo pip3 install --upgrade -e $GITHUB_WORKSPACE;
104+
echo "##[endgroup]";
105+
106+
#if [ "$GITHUB_REPOSITORY_OWNER" == "$GITHUB_ACTOR" ]; then
107+
if [ "$GITHUB_EVENT_NAME" == "push" ]; then
108+
echo "##[group] Uploading built wheels";
109+
python3 -m miniGHAPI artifact --name=$PACKAGE_FILE_NAME $PACKAGE_FILE_PATH;
110+
echo "##[endgroup]";
111+
else
112+
echo "Not uploading, event is $GITHUB_EVENT_NAME, not push";
113+
fi;
114+
#else
115+
# echo "Not uploading, not owner, owner is $GITHUB_REPOSITORY_OWNER , you are $GITHUB_ACTOR";
116+
#fi
117+
118+
TESTS_DIR=$GITHUB_WORKSPACE/tests;
119+
if [[ -d "$TESTS_DIR/" ]]; then
120+
if [ $SHOULD_ISOLATE_TESTING ]; then
121+
ISOLATE_TESTING="";
122+
else
123+
ISOLATE_TESTING=$ISOLATE;
124+
fi
125+
if [ $NEED_PYTEST ]; then
126+
echo "##[group] testing with pytest and computing coverage";
127+
$ISOLATE_TESTING coverage run --branch --source=$PACKAGE_NAME -m pytest --junitxml=./rspec.xml $TESTS_DIR/*.py;
128+
python3 -m miniGHAPI artifact --name=rspec.xml rspec.xml;
129+
else
130+
echo "##[group] testing without pytest and computing coverage";
131+
$ISOLATE_TESTING coverage run --branch --source=$PACKAGE_NAME $TESTS_DIR/*.py;
132+
echo "for rspec.xml you would need pytest";
133+
fi;
134+
echo "##[endgroup]";
135+
136+
if [[ -n "${INPUT_GITHUB_TOKEN}" ]]; then
137+
138+
if [ -d "$COVERAGEPY_ACTION_DIR" ]; then
139+
:
140+
else
141+
$ISOLATE git clone --depth=1 https://github.com/$COVERAGEPY_ACTION_REPO $COVERAGEPY_ACTION_DIR;
142+
fi;
143+
144+
INPUT_DATABASE_PATH=$GITHUB_WORKSPACE/.coverage INPUT_PACKAGE_NAME=$PACKAGE_NAME INPUT_PACKAGE_ROOT=$GITHUB_WORKSPACE bash $COVERAGEPY_ACTION_DIR/action.sh;
145+
else
146+
echo "No GitHub token is provided. If you want to annotate the code with coverage, set 'GITHUB_TOKEN' input variable";
147+
fi;
148+
149+
if [[ -n "${CODECOV_TOKEN}" ]]; then
150+
echo "##[group] Uploading coverage to codecov";
151+
$ISOLATE codecov || true;
152+
echo "##[endgroup]";
153+
fi;
154+
if [[ -n "${COVERALLS_REPO_TOKEN}" ]]; then
155+
echo "##[group] Uploading coverage to coveralls";
156+
$ISOLATE coveralls || true;
157+
echo "##[endgroup]";
158+
fi;
159+
fi;

action.yml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: typical-python-workflow
2+
description: |
3+
Does the usual worflow for python packages assumming a certain structure of the repo.
4+
author: KOLANICH
5+
6+
inputs:
7+
use_pytest:
8+
description: 'Whether to use pytest.'
9+
required: false
10+
default: true
11+
should_isolate_testing:
12+
description: 'Whether to isolate testing.'
13+
default: true
14+
github_token:
15+
description: 'Token to report coverage'
16+
required: false
17+
default: ""
18+
19+
runs:
20+
using: "node16"
21+
main: "startBash.js" # ECMA modules are not yet supported by GH Actions
22+
23+
branding:
24+
icon: tick
25+
color: green

getPackageName.py

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
import typing
3+
import sys
4+
from pathlib import Path
5+
6+
import tomli
7+
8+
# pylint:disable=unused-argument,import-outside-toplevel
9+
10+
PyprojectTOML_T = typing.Dict[str, typing.Union[str, int, list]]
11+
12+
13+
def extractFromPEP621(pyproject: PyprojectTOML_T) -> None:
14+
project = pyproject.get("project", None)
15+
if isinstance(project, dict):
16+
return project.get("name", None)
17+
18+
return None
19+
20+
21+
def getPackageName(rootDir: Path) -> str:
22+
tomlPath = Path(rootDir / "pyproject.toml")
23+
24+
if tomlPath.is_file():
25+
with tomlPath.open("rb") as f:
26+
pyproject = tomli.load(f)
27+
28+
fromPEP621 = extractFromPEP621(pyproject)
29+
if fromPEP621:
30+
return fromPEP621
31+
32+
buildBackend = pyproject["build-system"].get("build-backend", "setuptools.build_meta").split(".")[0]
33+
else:
34+
buildBackend = "setuptools"
35+
pyproject = None
36+
print("pyproject.toml is not present, falling back to", buildBackend, file=sys.stderr)
37+
38+
print("Build backend used: ", buildBackend, file=sys.stderr)
39+
40+
return toolSpecificExtractors[buildBackend](pyproject, rootDir)
41+
42+
43+
def extractFromFlit(pyproject: PyprojectTOML_T, rootDir: Path) -> str:
44+
tool = pyproject.get("tool", None)
45+
if isinstance(tool, dict):
46+
flit = tool.get("flit", None)
47+
if isinstance(flit, dict):
48+
metadata = flit.get("metadata", None)
49+
if isinstance(metadata, dict):
50+
name = metadata.get("dist-name", None)
51+
if name is None:
52+
name = metadata.get("module", None)
53+
if name:
54+
return name
55+
raise ValueError("Flit metadata is not present")
56+
57+
58+
def extractFromPoetry(pyproject, rootDir):
59+
tool = pyproject.get("tool", None)
60+
if isinstance(tool, dict):
61+
poetry = tool.get("poetry", None)
62+
if isinstance(poetry, dict):
63+
name = poetry.get("name", None)
64+
if name:
65+
return name
66+
raise ValueError("Poetry metadata is not present")
67+
68+
69+
def extractFromPDM(pyproject, rootDir):
70+
tool = pyproject.get("tool", None)
71+
if isinstance(tool, dict):
72+
pdm = tool.get("pdm", None)
73+
if isinstance(pdm, dict):
74+
name = pdm.get("name", None)
75+
if name:
76+
return name
77+
raise ValueError("PDM metadata is not present")
78+
79+
80+
def extractFromSetuptools(pyproject: PyprojectTOML_T, rootDir: Path) -> str:
81+
setupCfgPath = Path(rootDir / "setup.cfg")
82+
setupPyPath = Path(rootDir / "setup.py")
83+
84+
res = None
85+
86+
if setupCfgPath.is_file():
87+
res = extractFromSetupCfg(setupCfgPath)
88+
89+
if res:
90+
return res
91+
92+
if setupPyPath.is_file():
93+
return extractFromSetupPy(setupPyPath)
94+
95+
raise ValueError("setuptols metadata is not present")
96+
97+
98+
def extractFromSetupCfg(setupCfgPath: Path) -> typing.Optional[str]:
99+
from setuptools.config import read_configuration
100+
101+
setupCfg = read_configuration(setupCfgPath)
102+
try:
103+
return setupCfg["metadata"]["name"]
104+
except KeyError:
105+
return None
106+
107+
108+
def extractFromSetupPy(setupPyPath: Path) -> str:
109+
import ast
110+
111+
a = ast.parse(setupPyPath.read_text())
112+
113+
def findSetupCall(a):
114+
for n in ast.walk(a):
115+
if isinstance(n, ast.Call):
116+
f = n.func
117+
if isinstance(f, ast.Name):
118+
if f.id == "setup":
119+
return n
120+
121+
return None
122+
123+
def findNameKeyword(setupCall):
124+
for kw in setupCall.keywords:
125+
if kw.arg == "name":
126+
return kw.value
127+
128+
return None
129+
130+
setupCall = findSetupCall(a)
131+
nameAst = findNameKeyword(setupCall)
132+
133+
if isinstance(nameAst, ast.Name):
134+
raise ValueError("Not yet implemented")
135+
if isinstance(nameAst, ast.Str):
136+
return nameAst.value
137+
138+
139+
toolSpecificExtractors = {
140+
"setuptools": extractFromSetuptools,
141+
"flit_core": extractFromFlit,
142+
"poetry": extractFromPoetry,
143+
"pdm": extractFromPDM,
144+
}
145+
146+
147+
def main():
148+
if len(sys.argv) > 1:
149+
p = sys.argv[1]
150+
else:
151+
p = "."
152+
print(getPackageName(Path(p)), file=sys.stdout)
153+
154+
155+
if __name__ == "__main__":
156+
main()

0 commit comments

Comments
 (0)