Skip to content

Commit 61b0e9e

Browse files
authored
🎉 Single py checker (#10246)
1 parent 9163277 commit 61b0e9e

File tree

20 files changed

+328
-220
lines changed

20 files changed

+328
-220
lines changed

.github/actions/ci-java-tests/action.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ inputs:
99
runs:
1010
using: "composite"
1111
steps:
12-
- name: Install Java
13-
uses: actions/setup-java@v1
14-
with:
15-
java-version: '17'
16-
1712
- name: "Build"
1813
shell: bash
1914
run: |

.github/actions/ci-py-tests/action.yml

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,57 +27,26 @@ runs:
2727
- name: Build Coverage Reports
2828
id: build-coverage-reports
2929
shell: bash
30-
working-directory: ${{ inputs.module-folder }}
3130
run: |
32-
virtualenv .venv
33-
source .venv/bin/activate
34-
JSON_CONFIG='{"module": "${{ inputs.module-name }}", "folder": "${{ inputs.module-folder }}", "lang": "py"}'
35-
pip install coverage[toml]~=6.2
36-
mkdir -p .venv/source-acceptance-test
37-
mkdir -p reports
38-
SAT_DIR=$(git rev-parse --show-toplevel)/airbyte-integrations/bases/source-acceptance-test
39-
PYPROJECT_CONFIG=$(git rev-parse --show-toplevel)/pyproject.toml
40-
git ls-tree -r HEAD --name-only $SAT_DIR | while read src; do cp -f $src .venv/source-acceptance-test; done
41-
pip install build
42-
python -m build .venv/source-acceptance-test
43-
pip install .venv/source-acceptance-test/dist/source_acceptance_test-*.whl
44-
[ -f requirements.txt ] && pip install --quiet -r requirements.txt
45-
pip install .[tests]
46-
coverage run --rcfile=${PYPROJECT_CONFIG} -m pytest ./unit_tests || true
47-
coverage xml --rcfile=${PYPROJECT_CONFIG} -o reports/coverage.xml || true
31+
GRADLE_JOB=$(source ./tools/lib/lib.sh; full_path_to_gradle_path ${{ inputs.module-folder }} "unitTest")
32+
REPORT_FOLDER="${{ inputs.module-folder }}/coverage/"
33+
./gradlew --no-daemon -Preports_folder=${REPORT_FOLDER} ${GRADLE_JOB}
4834
49-
rm -rf .venv
50-
echo "::set-output name=coverage-paths::reports/coverage.xml"
35+
echo "::set-output name=coverage-paths::coverage/coverage.xml"
5136
5237
- name: Upload coverage to Codecov
53-
if: ${{ always() }}
5438
uses: codecov/codecov-action@v2
39+
with:
40+
file: ${{ steps.build-coverage-reports.outputs.coverage-paths }}
41+
name: "UnitTests of ${{ inputs.module-name }}"
5542

5643
- name: Build Linter Reports
5744
id: build-linter-reports
5845
shell: bash
59-
working-directory: ${{ inputs.module-folder }}
6046
run: |
61-
JSON_CONFIG='{"module": "${{ inputs.module-name }}", "folder": "${{ inputs.module-folder }}", "lang": "py"}'
62-
REPORT_FOLDER=reports
63-
PYPROJECT_CONFIG=$(git rev-parse --show-toplevel)/pyproject.toml
64-
65-
# run mypy
66-
pip install lxml~=4.7 mypy~=0.910 .
67-
mypy . --config-file=${PYPROJECT_CONFIG} | tee reports/mypy.log || true
68-
69-
# run black
70-
pip install black~=21.12b0
71-
XDG_CACHE_HOME=/dev/null black --config ${PYPROJECT_CONFIG} --diff . | tee reports/black.diff
72-
73-
# run isort
74-
pip install isort~=5.10.1
75-
cp ${PYPROJECT_CONFIG} ./pyproject.toml
76-
isort --diff . | tee reports/isort.diff
77-
78-
# run flake8
79-
pip install mccabe~=0.6.1 pyproject-flake8~=0.0.1a2
80-
pflake8 --exit-zero . | grep ^. | tee reports/flake.txt
47+
GRADLE_JOB=$(source ./tools/lib/lib.sh; full_path_to_gradle_path ${{ inputs.module-folder }} "airbytePythonReport")
48+
REPORT_FOLDER="${{ inputs.module-folder }}/reports/"
49+
./gradlew --no-daemon -Preports_folder=${REPORT_FOLDER} ${GRADLE_JOB}
8150
8251
echo "::set-output name=mypy-logs::reports/mypy.log"
8352
echo "::set-output name=black-diff::reports/black.diff"

.github/actions/ci-tests-runner/action.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,18 @@ runs:
3232
with:
3333
python-version: 3.7
3434

35+
- name: Install Java
36+
uses: actions/setup-java@v1
37+
with:
38+
java-version: '17'
39+
3540
- name: Tests of CI
3641
shell: bash
3742
run: |
3843
# all CI python packages have the prefix "ci_"
3944
pip install --quiet tox==3.24.4
40-
tox -r -c ./tools/tox_ci.ini
4145
pip install --quiet -e ./tools/ci_*
46+
tox -r -c ./tools/tox_ci.ini
4247
echo "::echo::off"
4348
4449
- name: Auth with gcloud CLI
@@ -109,6 +114,7 @@ runs:
109114
[ -f ${{ steps.ci-py-tests.outputs.coverage-paths }} ] && OPTIONS+=("-Dsonar.python.coverage.reportPaths=${{ steps.ci-py-tests.outputs.coverage-paths }}")
110115
[ -f ${{ steps.ci-py-tests.outputs.flake8-logs }} ] && OPTIONS+=("-Dsonar.python.flake8.reportPaths=${{ steps.ci-py-tests.outputs.flake8-logs }}")
111116
fi
117+
cat ${REPORT_FOLDER}/*
112118
if [ ${{ inputs.module-lang }} == 'java' ]; then
113119
[ -d "./src/main/java" ] && OPTIONS+=("-Dsonar.sources=./src/main/java")
114120
[ -d "./src/test/java" ] && OPTIONS+=("-Dsonar.tests=./src/test/java")
@@ -133,6 +139,7 @@ runs:
133139
MODULE_DIR=$(python -c "print('${{ inputs.module-folder }}'.replace('${ROOT_DIR}', '.'))")
134140
echo "::set-output name=module_dir::${MODULE_DIR}"
135141
142+
136143
- name: SonarQube Scan
137144

138145
uses: sonarsource/sonarqube-scan-action@master
@@ -148,7 +155,7 @@ runs:
148155
-Dsonar.language=${{ inputs.module-lang }}
149156
-Dsonar.sourceEncoding=UTF-8
150157
-Dsonar.projectBaseDir=${{ steps.create-sq-project.outputs.module_dir }}
151-
-Dsonar.exclusions=reports/**,*.toml
158+
-Dsonar.exclusions=reports/**,*.toml,*_tests/**,setup.py,main.py
152159
-Dsonar.externalIssuesReportPaths=${{ steps.sq-options.outputs.external_reports }}
153160
${{ steps.sq-options.outputs.options }}
154161

.github/workflows/sonar-scan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
sonar-token: ${{ secrets.SONAR_TOKEN }}
7373
sonar-gcp-access-key: ${{ secrets.GCP_SONAR_SA_KEY }}
7474
pull-request-id: "${{ github.repository }}/${{ github.event.pull_request.number }}"
75-
remove-sonar-project: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' }}
75+
remove-sonar-project: ${{ github.event.action == 'closed' }}
7676

7777

7878

buildSrc/src/main/groovy/airbyte-python.gradle

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,29 @@ class Helpers {
2929
3030
See https://github.com/airbytehq/airbyte/issues/4979 for original context
3131
"""
32-
if (project.file(testFilesDirectory).exists()) {
3332

33+
if (project.file(testFilesDirectory).exists()) {
34+
def outputArg = project.hasProperty('reports_folder') ?"-otemp_coverage.xml" : "--skip-empty"
35+
def coverageFormat = project.hasProperty('reports_folder') ? 'xml' : 'report'
36+
def testConfig = project.file('pytest.ini').exists() ? 'pytest.ini' : project.rootProject.file('pyproject.toml').absolutePath
3437
project.projectDir.toPath().resolve(testFilesDirectory).traverse(type: FileType.FILES, nameFilter: ~/(^test_.*|.*_test)\.py$/) { file ->
3538
project.task("_${taskName}Coverage", type: PythonTask, dependsOn: taskDependencies) {
3639
module = "coverage"
37-
command = "run --data-file=${testFilesDirectory}/.coverage.${taskName} --rcfile=${project.rootProject.file('tools/python/.coveragerc').absolutePath} -m pytest -s ${testFilesDirectory}"
40+
command = "run --data-file=${testFilesDirectory}/.coverage.${taskName} --rcfile=${project.rootProject.file('pyproject.toml').absolutePath} -m pytest -s ${testFilesDirectory} -c ${testConfig}"
3841
}
3942
// generation of coverage report is optional and we should skip it if tests are empty
43+
4044
project.task(taskName, type: Exec){
4145
commandLine = ".venv/bin/python"
42-
args "-m", "coverage", "report", "--data-file=${testFilesDirectory}/.coverage.${taskName}", "--rcfile=${project.rootProject.file('tools/python/.coveragerc').absolutePath}"
46+
args "-m", "coverage", coverageFormat, "--data-file=${testFilesDirectory}/.coverage.${taskName}", "--rcfile=${project.rootProject.file('pyproject.toml').absolutePath}", outputArg
4347
dependsOn project.tasks.findByName("_${taskName}Coverage")
4448
setIgnoreExitValue true
49+
doLast {
50+
// try to move a generated report to custom report folder if needed
51+
if (project.file('temp_coverage.xml').exists() && project.hasProperty('reports_folder')) {
52+
project.file('temp_coverage.xml').renameTo(project.file("${project.reports_folder}/coverage.xml"))
53+
}
54+
}
4555

4656
}
4757
// If a file is found, terminate the traversal, thus causing this task to be declared at most once
@@ -63,6 +73,7 @@ class AirbytePythonPlugin implements Plugin<Project> {
6373

6474
void apply(Project project) {
6575
def extension = project.extensions.create('airbytePython', AirbytePythonConfiguration)
76+
6677
def venvDirectoryName = '.venv'
6778
project.plugins.apply 'ru.vyarus.use-python'
6879

@@ -71,34 +82,63 @@ class AirbytePythonPlugin implements Plugin<Project> {
7182
minPythonVersion = '3.7'
7283
scope = 'VIRTUALENV'
7384
installVirtualenv = true
74-
pip 'flake8:3.8.4'
75-
pip 'black:21.10b0'
76-
pip 'mypy:0.812'
85+
pip 'pip:21.3.1'
86+
pip 'mccabe:0.6.1'
87+
// flake8 doesn't support pyproject.toml files
88+
// and thus there is the wrapper "pyproject-flake8" for this
89+
pip 'pyproject-flake8:0.0.1a2'
90+
pip 'black:22.1.0'
91+
pip 'mypy:0.930'
7792
pip 'isort:5.6.4'
7893
pip 'pytest:6.1.2'
79-
pip 'pip:21.1.3'
8094
pip 'coverage[toml]:6.3.1'
8195
}
8296

8397

8498
project.task('isortFormat', type: PythonTask) {
8599
module = "isort"
86-
command = ". --settings-file ${project.rootProject.file('tools/python/.isort.cfg').absolutePath}"
100+
command = "--settings-file=${project.rootProject.file('pyproject.toml').absolutePath} ./"
101+
}
102+
103+
project.task('isortReport', type: PythonTask) {
104+
module = "isort"
105+
command = "--settings-file=${project.rootProject.file('pyproject.toml').absolutePath} --diff --quiet ./"
106+
outputPrefix = ''
87107
}
88108

89109
project.task('blackFormat', type: PythonTask) {
90110
module = "black"
91111
// the line length should match .isort.cfg
92-
command = ". --line-length 140"
112+
command = "--config ${project.rootProject.file('pyproject.toml').absolutePath} ./"
93113
dependsOn project.rootProject.licenseFormat
94114
dependsOn project.isortFormat
95115
}
96116

117+
project.task('blackReport', type: PythonTask) {
118+
module = "black"
119+
command = "--config ${project.rootProject.file('pyproject.toml').absolutePath} --diff --quiet ./"
120+
outputPrefix = ''
121+
}
122+
97123
project.task('flakeCheck', type: PythonTask, dependsOn: project.blackFormat) {
98-
module = "flake8"
99-
command = ". --config ${project.rootProject.file('tools/python/.flake8').absolutePath}"
124+
module = "pflake8"
125+
command = "--config ${project.rootProject.file('pyproject.toml').absolutePath} ./"
126+
}
127+
128+
project.task('flakeReport', type: PythonTask) {
129+
module = "pflake8"
130+
command = "--exit-zero --config ${project.rootProject.file('pyproject.toml').absolutePath} ./"
131+
outputPrefix = ''
132+
}
133+
134+
project.task("mypyReport", type: Exec){
135+
commandLine = ".venv/bin/python"
136+
args "-m", "mypy", "--config-file", "${project.rootProject.file('pyproject.toml').absolutePath}", "./"
137+
setIgnoreExitValue true
100138
}
101139

140+
141+
102142
// attempt to install anything in requirements.txt. by convention this should only be dependencies whose source is located in the project.
103143

104144
if (project.file('requirements.txt').exists()) {
@@ -109,7 +149,7 @@ class AirbytePythonPlugin implements Plugin<Project> {
109149
outputs.file('build/installedlocalreqs.txt')
110150

111151
// HACK: makes all integrations depend on installing requirements for bases. long term we should resolve deps and install in order.
112-
if (project.getPath().startsWith(":airbyte-integrations:connectors")) {
152+
if (project.getPath().startsWith(":airbyte-integrations:connectors") && !project.hasProperty("reports_folder")) {
113153
dependsOn project.rootProject.getTasksByName("airbytePythonApply", true).findAll { it.project.getPath().startsWith(":airbyte-integrations:bases") }
114154
}
115155
}
@@ -139,6 +179,7 @@ class AirbytePythonPlugin implements Plugin<Project> {
139179
}
140180

141181
Helpers.addTestTaskIfTestFilesFound(project, 'unit_tests', 'unitTest', project.installTestReqs)
182+
142183
Helpers.addTestTaskIfTestFilesFound(project, 'integration_tests', 'customIntegrationTests', project.installTestReqs)
143184
if (!project.tasks.findByName('integrationTest')) {
144185
project.task('integrationTest')
@@ -148,7 +189,7 @@ class AirbytePythonPlugin implements Plugin<Project> {
148189
if (extension.moduleDirectory) {
149190
project.task('mypyCheck', type: PythonTask) {
150191
module = "mypy"
151-
command = "-m ${extension.moduleDirectory} --config-file ${project.rootProject.file('tools/python/.mypy.ini').absolutePath}"
192+
command = "-m ${extension.moduleDirectory} --config-file ${project.rootProject.file('pyproject.toml').absolutePath}"
152193
}
153194

154195
project.check.dependsOn mypyCheck
@@ -160,6 +201,36 @@ class AirbytePythonPlugin implements Plugin<Project> {
160201
dependsOn project.flakeCheck
161202
}
162203

204+
project.task('airbytePythonReport', type: DefaultTask) {
205+
dependsOn project.blackReport
206+
dependsOn project.isortReport
207+
dependsOn project.flakeReport
208+
dependsOn project.mypyReport
209+
doLast {
210+
if (project.hasProperty('reports_folder')) {
211+
// Gradles adds some log messages to files and we must remote them
212+
// examples of these lines:
213+
// :airbyte-integrations:connectors: ...
214+
// [python] .venv/bin/python -m black ...
215+
project.fileTree(project.reports_folder).visit { FileVisitDetails details ->
216+
project.println "Found the report file: " + details.file.path
217+
def tempFile = project.file(details.file.path + ".1")
218+
details.file.eachLine { line ->
219+
if ( !line.startsWith(":airbyte") && !line.startsWith("[python]") ) {
220+
tempFile << line + "\n"
221+
}
222+
}
223+
if (!tempFile.exists()) {
224+
// generate empty file
225+
tempFile << "\n"
226+
}
227+
tempFile.renameTo(details.file)
228+
229+
}
230+
}
231+
}
232+
}
233+
163234
project.task('airbytePythonApply', type: DefaultTask) {
164235
dependsOn project.installReqs
165236
dependsOn project.airbytePythonFormat
@@ -187,5 +258,53 @@ class AirbytePythonPlugin implements Plugin<Project> {
187258
project.assemble.dependsOn project.airbytePythonApply
188259
project.assemble.dependsOn project.airbytePythonTest
189260
project.test.dependsOn project.airbytePythonTest
261+
262+
// saves tools reports to a custom folder
263+
def reportsFolder = project.hasProperty('reports_folder') ? project.reports_folder : ''
264+
if ( reportsFolder != '' ) {
265+
266+
// clean reports folders
267+
project.file(reportsFolder).deleteDir()
268+
project.file(reportsFolder).mkdirs()
269+
270+
271+
272+
project.tasks.blackReport.configure {
273+
it.logging.addStandardOutputListener(new StandardOutputListener() {
274+
@Override
275+
void onOutput(CharSequence charSequence) {
276+
project.file("$reportsFolder/black.diff") << charSequence
277+
}
278+
})
279+
}
280+
project.tasks.isortReport.configure {
281+
it.logging.addStandardOutputListener(new StandardOutputListener() {
282+
@Override
283+
void onOutput(CharSequence charSequence) {
284+
project.file("$reportsFolder/isort.diff") << charSequence
285+
}
286+
})
287+
}
288+
289+
project.tasks.flakeReport.configure {
290+
it.logging.addStandardOutputListener(new StandardOutputListener() {
291+
@Override
292+
void onOutput(CharSequence charSequence) {
293+
project.file("$reportsFolder/flake.txt") << charSequence
294+
}
295+
})
296+
}
297+
298+
project.tasks.mypyReport.configure {
299+
it.logging.addStandardOutputListener(new StandardOutputListener() {
300+
@Override
301+
void onOutput(CharSequence charSequence) {
302+
project.file("$reportsFolder/mypy.log") << charSequence
303+
}
304+
})
305+
}
306+
307+
}
190308
}
191309
}
310+

0 commit comments

Comments
 (0)