From a9527b100ca20150178df6b0cd23dfb8d02f0416 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Sat, 5 Sep 2020 16:44:32 +0900 Subject: [PATCH 01/14] build(ci): remove Travis and AppVeyor --- .build/install_pyenv.sh | 20 ----------- .travis.yml | 76 ----------------------------------------- appveyor.yml | 55 ----------------------------- 3 files changed, 151 deletions(-) delete mode 100755 .build/install_pyenv.sh delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.build/install_pyenv.sh b/.build/install_pyenv.sh deleted file mode 100755 index 018a54e..0000000 --- a/.build/install_pyenv.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -Eeuxo pipefail - -# pyenv installer (for macOS) -# updated: 2020-09-05 - -# use the following to enable diagnostics -# export PYENV_DIAGNOSE=1 - -if [[ "$(uname)" == "Darwin" ]]; then - if [ -n "${DIAGNOSE_PYENV-}" ] ; then - pyenv install --list - fi - if ! [[ ${TRAVIS_PYTHON_VERSION} =~ .*-dev$ ]] ; then - TRAVIS_PYTHON_VERSION="$(pyenv install --list | grep -E " ${TRAVIS_PYTHON_VERSION}(\.[0-9brc]+)+" | tail -n 1 | sed -e 's/^[[:space:]]*//')" - fi - pyenv install "${TRAVIS_PYTHON_VERSION}" - pyenv global "${TRAVIS_PYTHON_VERSION}" - echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bash_profile -fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a7d46a8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,76 +0,0 @@ -language: generic -addons: - homebrew: - packages: - - pyenv -jobs: - include: - - os: linux - language: python - python: "3.6" - - os: linux - language: python - python: "3.7" - - os: linux - language: python - python: "3.8" - - os: linux - language: python - python: "3.9-dev" - - os: linux - language: python - python: "pypy3.6-7.1.1" - - os: osx - osx_image: xcode12u - language: generic - env: TRAVIS_PYTHON_VERSION="3.6" - - os: osx - osx_image: xcode12u - language: generic - env: TRAVIS_PYTHON_VERSION="3.7" - - os: osx - osx_image: xcode12u - language: generic - env: TRAVIS_PYTHON_VERSION="3.8" - -before_install: - - DIAGNOSE_PYENV=1 .build/install_pyenv.sh - - if [[ "$(uname)" == "Darwin" ]]; then source ~/.bash_profile; fi - -install: - - pip install -U pip - - pip install -U -r ci_requirements.txt - # just some example Python packages - - git clone https://github.com/PyCQA/pylint ../pylint - - cd ../pylint && python setup.py build && cd - - - git clone https://github.com/mbdevpl/argunparse ../argunparse - - cd ../argunparse && pip install -r test_requirements.txt && python setup.py build && cd - - - git clone https://github.com/k-bx/python-semver ../semver - - cd ../semver && python setup.py build && cd - - -script: - - TEST_PACKAGING=1 python -m coverage run --branch --source . -m unittest -v - - LOGGING_LEVEL=critical python -m coverage run --append --branch --source . -m unittest -v test.test_version - -after_success: - - python -m coverage report --show-missing - - codecov - -before_deploy: - - pip install -U version_query - - wget https://gist.githubusercontent.com/mbdevpl/46d458350f0c9cc7d793b67573e01f7b/raw/prepare_bintray_deployment.py - - python prepare_bintray_deployment.py "$TRAVIS_OS_NAME-python$TRAVIS_PYTHON_VERSION" "dist/*.tar.gz" "dist/*.whl" "dist/*.zip" - -deploy: - - provider: bintray - file: ".bintray.json" - user: "mbdevpl" - key: - secure: "nCFnq4OedRxTWRk0mgZP0PJiFVRsNQXV2kII2EEq2fMz64I4R4PrHf+oWUYt5P+64H4OFE+IqDFMJfb0S1Dgt2IF480+Hz1t+jWDzeyhbzBIa4XkbnhkDH6ChgTW/Mv/rijjgvXBrBoUhFKqGphJ8SAIVISd2KSB5ciEoWnyJ8nyy6sXxOKFbxMXYaTIkDazRZdNzvQJ+6p9KwbEYnOG4cRddklvejn4iWFKKVcfZEtnkY0cMEUdmHjmr6kJqF2S5XhFZ8I6n0cVfu12ja2kMrJY2ljWiqjnUUTBEyAKPvOXYlMdfXo3edEUE+FmCKUMNNo21Dv/4hoM978tIiu5P4jbd9NIqULncIyPvSq/kbV/2CG9xSCwl9OL7uTvZqaoe/QpUX140cO60jkkDf0YqXpk0sGuXIE+xfYNMRsH+BzSmDD+TGadJD63GrdNOPPyEDW+NKSis3DzMLiAdVOYstoIUdd07DW+4tS7JKzMzOdruUOve6Uq+PvwNObcTi3vZrOl98NIu6DTYyXTe/sb2If8WBpnmkfl8Ua/Q0KjcLLONiespxJxtMfyuFiN5l+tFIC3pWGK5+QA9DnM5FTkdXwkbqGdqmvK17ponHgKhjjr8YVGVYoXu4G+2mRhWWCPe5PFQbPQtM2br4j+wCugmPL1qkfFXcJ6Oavjqxpg7y8=" - on: - all_branches: true - skip_cleanup: true - -notifications: - slack: - secure: "m10wMO0tqsggvtlJ+jdbQJQSfij/pa2nlr6LQsiAHUENeydVSa0ZejHjPRwbIoVYi+VXe3WAON6K4fKziBahEv92Z3PokGEnCE3AiAwXHM2F2dNuJqeug0UW7VZcEMAXK+yUW2ZTlJUTVDZg4A8EVCbV5lJ1cR61XisVEtk/DmC9lyms1UXNerUCImPlGwk6c0NbXYieZdMV1qljvuqZWjC8ZhgLle44NHPptDgltlRha9Qh+mh0CpoF4zTfXmSD0T2RLUygJ0iawL1fTmpxjn0QvbIo99KHkXwHir08S8zwjPwcBqm8cNue+BUfmnJFZbY31uOdhhYMKNTJu+pcdsN/6WfezKvDS/uLiOk0QCtZRm9oaSQxCh/wGgPIlTCO2Ui5RuK9iWo8rc6Kn0kdu2roCWwdV1sBiDbcJ4CPQoyadSVSqJTQswNwxUytDtAnlygcAwEiTREMoe3gyO5bSv0W29K2chL3cbBmaTQ+F6TuOPxVBkaezgRO7IMN52tht14obcLMuByccDVkUKJ2pG8rwgdwPZ8I6gcYdP0KgQs9d8Q5PMSaiwM8o/zmGTfvQqcbyCKaswwiAJEpq5g6rNsaXL2c9BV7CLMGBouy2UFPckqtC+4AnLWj6hiGYQGdidKzZ/eCjOG3W/HY/H/GPdvwPBx43jFURk8ZdKJBrVs=" diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index e02b640..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,55 +0,0 @@ -version: "{build}" - -environment: - matrix: - - ARCHITECTURE: "x64" - PYTHON_VERSION: "3.6" - PYTHON: "C:\\Python36-x64" - - ARCHITECTURE: "x64" - PYTHON_VERSION: "3.7" - PYTHON: "C:\\Python37-x64" - - ARCHITECTURE: "x64" - PYTHON_VERSION: "3.8" - PYTHON: "C:\\Python38-x64" - -init: - - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% - -install: - - python -m pip install -U pip - - python -m pip install -U -r ci_requirements.txt - -build: off - -test_script: - - set TEST_PACKAGING=1 - - python -m coverage run --branch --source . -m unittest -v - -after_test: - - python -m coverage report --show-missing - - codecov - # Bintray archive preparation - - python -m pip install version_query - - ps: Invoke-WebRequest "https://gist.githubusercontent.com/mbdevpl/46d458350f0c9cc7d793b67573e01f7b/raw/prepare_bintray_deployment.py" -OutFile "prepare_bintray_deployment.py" - - python prepare_bintray_deployment.py "windows%ARCHITECTURE%-python%PYTHON_VERSION%" "dist\*.tar.gz" "dist\*.whl" "dist\*.zip" - - set /p BINTRAY_VERSION=<.bintray_version.txt - -artifacts: - - path: dist\*.tar.gz - - path: dist\*.whl - - path: dist\*.zip - - path: '*-bintray.zip' - -deploy: - - provider: BinTray - username: $(APPVEYOR_ACCOUNT_NAME) - api_key: - secure: cMLbWadS24XyCD5RU3XM+2GrgqtTfoBgKwkQXyDyVa/3QOF1rXheHki+BRXP5tLo - subject: $(APPVEYOR_ACCOUNT_NAME) - repo: pkgs - package: $(APPVEYOR_PROJECT_NAME) - version: $(BINTRAY_VERSION) - publish: true - override: true - explode: true - artifact: /.*-bintray\.zip/ From 6b8eb5d5389401c61cf0158f85533f3b2bf16ff1 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 21:32:32 +0900 Subject: [PATCH 02/14] build(requirements): update requirements --- MANIFEST.in | 2 +- ci_requirements.txt | 3 --- requirements.txt | 8 ++++---- requirements_ci.txt | 9 +++++++++ requirements_test.txt | 6 ++++++ test_requirements.txt | 6 ------ 6 files changed, 20 insertions(+), 14 deletions(-) delete mode 100644 ci_requirements.txt create mode 100644 requirements_ci.txt create mode 100644 requirements_test.txt delete mode 100644 test_requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index ffd48d3..7cb9b01 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,6 @@ include LICENSE include NOTICE include ./*/py.typed -include test_requirements.txt +include requirements_test.txt recursive-include ./test __init__.py include test/examples.py diff --git a/ci_requirements.txt b/ci_requirements.txt deleted file mode 100644 index 2bd9828..0000000 --- a/ci_requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -codecov >= 2.0.15 -coverage >= 5.0.3 --rtest_requirements.txt diff --git a/requirements.txt b/requirements.txt index 13a40d6..210435c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -GitPython ~= 3.0 -packaging >= 20.1 -semver ~= 2.9 -setuptools >= 45.1 +GitPython ~= 3.1 +packaging >= 21.0 +semver ~= 2.13 +setuptools >= 60.4 diff --git a/requirements_ci.txt b/requirements_ci.txt new file mode 100644 index 0000000..61e203f --- /dev/null +++ b/requirements_ci.txt @@ -0,0 +1,9 @@ +codecov ~= 2.1 +coverage ~= 6.2 +mypy ~= 0.930 +pycodestyle ~= 2.8 +pydocstyle ~= 6.1 +pylint ~= 2.12 +types-docutils ~= 0.17 +types-setuptools ~= 57.4 +-r requirements_test.txt diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..3387323 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,6 @@ +colorlog ~= 6.6 +docutils >= 0.18 +pip >= 21.0 +Pygments >= 2.5 +wheel >= 0.37 +-r requirements.txt diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index a0270d7..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -colorlog >= 4.1 -docutils >= 0.16 -pip >= 20.0 -Pygments >= 2.5 -wheel >= 0.34 --rrequirements.txt From 9b713d4d84d9c25d71018152086e2f1d0e981148 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 21:56:28 +0900 Subject: [PATCH 03/14] build(ci): add GitHub actions --- .github/workflows/python.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..6c7db6a --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,35 @@ +name: tests + +on: + push: + pull_request: + branches: + - $default-branch + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - run: pip install -r requirements_ci.txt + - run: git clone https://github.com/PyCQA/pycodestyle ../pycodestyle + - run: cd ../pycodestyle && python setup.py build && cd - + - run: git clone https://github.com/mbdevpl/argunparse ../argunparse + - run: cd ../argunparse && pip install -r test_requirements.txt && python setup.py build && cd - + - run: git clone https://github.com/python-semver/python-semver ../semver + - run: cd ../semver && python setup.py build && cd - + - run: python -m coverage run --branch --source . -m unittest -v + # - run: LOGGING_LEVEL=critical python -m coverage run --append --branch --source . -m unittest -v test.test_version + - run: python -m coverage report --show-missing + - run: codecov From 6f99f286f93788754a6ab448e5d8b69f5eee1e02 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 22:53:13 +0900 Subject: [PATCH 04/14] test(query): enable all packaging tests on CI --- test/test_query.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index bbddc4f..2cf636d 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -101,7 +101,8 @@ def test_query_pkg_info(self): self._check_examples_count('PKG-INFO', PKG_INFO_EXAMPLE_PATHS) self._query_test_case(PKG_INFO_EXAMPLE_PATHS, query_pkg_info) - @unittest.skipUnless(os.environ.get('TEST_PACKAGING'), 'skipping packaging test') + @unittest.skipUnless( + os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 'skipping packaging test') def test_query_pkg_info_current(self): with preserve_logger_level('system_query'): run_module('setup', 'build') @@ -133,7 +134,8 @@ def test_query_package_folder(self): self._check_examples_count('package folder', PACKAGE_FOLDER_EXAMPLES) self._query_test_case(PACKAGE_FOLDER_EXAMPLES, query_package_folder) - @unittest.skipUnless(os.environ.get('TEST_PACKAGING'), 'skipping packaging test') + @unittest.skipUnless( + os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 'skipping packaging test') def test_query_package_folder_current(self): with preserve_logger_level('system_query'): run_module('setup', 'build') From 1d041f6c029063bc298151cadb1d336b6a1160ab Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 22:53:33 +0900 Subject: [PATCH 05/14] docs(readme): update repo badges --- README.rst | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index bbb1ddd..78b2b3e 100644 --- a/README.rst +++ b/README.rst @@ -15,22 +15,18 @@ Zero-overhead package versioning for Python. :target: https://pypi.org/project/version-query :alt: package version from PyPI -.. image:: https://travis-ci.com/mbdevpl/version-query.svg?branch=master - :target: https://travis-ci.com/mbdevpl/version-query - :alt: build status from Travis CI +.. image:: https://github.com/mbdevpl/version-query/actions/workflows/python.yml/badge.svg?branch=main + :target: https://github.com/mbdevpl/version-query/actions + :alt: build status from GitHub -.. image:: https://ci.appveyor.com/api/projects/status/github/mbdevpl/version-query?branch=master&svg=true - :target: https://ci.appveyor.com/project/mbdevpl/version-query - :alt: build status from AppVeyor +.. image:: https://codecov.io/gh/mbdevpl/version-query/branch/master/graph/badge.svg + :target: https://codecov.io/gh/mbdevpl/version-query + :alt: test coverage from Codecov .. image:: https://api.codacy.com/project/badge/Grade/437ab82bd6324530847fe8ed833f8d78 :target: https://www.codacy.com/app/mbdevpl/version-query :alt: grade from Codacy -.. image:: https://codecov.io/gh/mbdevpl/version-query/branch/master/graph/badge.svg - :target: https://codecov.io/gh/mbdevpl/version-query - :alt: test coverage from Codecov - .. image:: https://img.shields.io/github/license/mbdevpl/version-query.svg :target: NOTICE :alt: license From a875f9c00ff34fafd12560587f39187cdc0ec9b1 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 22:54:17 +0900 Subject: [PATCH 06/14] build: drop Python 3.6 and 3.7 support --- README.rst | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 78b2b3e..b1c5a5e 100644 --- a/README.rst +++ b/README.rst @@ -436,7 +436,7 @@ using version-query without any issues. Requirements ============ -Python version 3.6 or later. +Python version 3.8 or later. Python libraries as specified in ``_. diff --git a/setup.py b/setup.py index 4e42244..bc440d8 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ class Package(setup_boilerplate.Package): 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development :: Version Control', 'Topic :: Software Development :: Version Control :: Git', From 1d7e511334e68156240331b879ff05d91a15ee69 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 23:00:49 +0900 Subject: [PATCH 07/14] test(query): preserve level of the relevant logger --- test/test_query.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_query.py b/test/test_query.py index 2cf636d..b31d092 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -104,7 +104,7 @@ def test_query_pkg_info(self): @unittest.skipUnless( os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 'skipping packaging test') def test_query_pkg_info_current(self): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): run_module('setup', 'build') paths = list(pathlib.Path.cwd().glob('*.egg-info/PKG-INFO')) self.assertEqual(len(paths), 1) @@ -137,7 +137,7 @@ def test_query_package_folder(self): @unittest.skipUnless( os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 'skipping packaging test') def test_query_package_folder_current(self): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): run_module('setup', 'build') path = pathlib.Path.cwd().joinpath('version_query') version = query_package_folder(path) @@ -164,7 +164,7 @@ def test_not_as_main(self): # pylint: disable = no-self-use def test_help(self): sio = io.StringIO() with contextlib.redirect_stderr(sio): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): with self.assertRaises(SystemExit): run_module('version_query') _LOG.info('%s', sio.getvalue()) @@ -172,7 +172,7 @@ def test_help(self): def test_bad_usage(self): sio = io.StringIO() with contextlib.redirect_stderr(sio): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): with self.assertRaises(ValueError): run_module('version_query', '-p', '-i', '.') _LOG.info('%s', sio.getvalue()) @@ -180,7 +180,7 @@ def test_bad_usage(self): def test_here(self): sio = io.StringIO() with contextlib.redirect_stdout(sio): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): run_module('version_query', '.') self.assertEqual(sio.getvalue().rstrip(), query_caller().to_str()) self.assertEqual(sio.getvalue().rstrip(), query_version_str()) @@ -188,7 +188,7 @@ def test_here(self): def test_increment_here(self): sio = io.StringIO() with contextlib.redirect_stdout(sio): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): run_module('version_query', '-i', '.') self.assertEqual(sio.getvalue().rstrip(), query_caller().increment(VersionComponent.Patch).to_str()) @@ -196,7 +196,7 @@ def test_increment_here(self): def test_predict_here(self): sio = io.StringIO() with contextlib.redirect_stdout(sio): - with preserve_logger_level('system_query'): + with preserve_logger_level('version_query'): run_module('version_query', '-p', '.') self.assertEqual(sio.getvalue().rstrip(), predict_caller().to_str()) self.assertEqual(sio.getvalue().rstrip(), predict_version_str()) From 5033987e3eb91aa28d61429a7b3d064c38dbac2a Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 23:20:15 +0900 Subject: [PATCH 08/14] test(examples): set examples folder from envvar --- test/__init__.py | 3 +++ test/examples.py | 11 ++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 46d50b2..2e3c473 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -14,3 +14,6 @@ logging.getLogger('version_query').setLevel( getattr(logging, os.environ.get('LOGGING_LEVEL', 'debug').upper())) logging.getLogger('test').setLevel(logging.DEBUG) + +if 'EXAMPLE_PROJECTS_PATH' not in os.environ: + os.environ['EXAMPLE_PROJECTS_PATH'] = '..' diff --git a/test/examples.py b/test/examples.py index 3c31982..472e1f1 100644 --- a/test/examples.py +++ b/test/examples.py @@ -1,6 +1,7 @@ """Examples for tests.""" import itertools +import os import pathlib import platform import sys @@ -10,11 +11,7 @@ _HERE = pathlib.Path(__file__).resolve().parent -_PACKAGE_FOLDER = _HERE.parent - -_GIT_REPOS_ROOT = _PACKAGE_FOLDER.parent -if platform.system() != 'Windows': - _GIT_REPOS_ROOT = _GIT_REPOS_ROOT.parent +_GIT_REPOS_ROOT = pathlib.Path(os.environ['EXAMPLE_PROJECTS_PATH']).absolute() GIT_REPO_EXAMPLES = list(_ for _ in _GIT_REPOS_ROOT.glob('**/.git') if _.is_dir()) @@ -42,8 +39,8 @@ def python_lib_dir() -> pathlib.Path: _SYS_DIST_INFOS = list(PY_LIB_DIR.glob('**/*.dist-info')) _SYS_EGG_INFOS = list(PY_LIB_DIR.glob('**/*.egg-info')) -_USER_DIST_INFOS = [pth for _ in _PACKAGE_FOLDER.parent.glob('*') for pth in _.glob('*.dist-info')] -_USER_EGG_INFOS = [pth for _ in _PACKAGE_FOLDER.parent.glob('*') for pth in _.glob('*.egg-info')] +_USER_DIST_INFOS = [pth for _ in _GIT_REPOS_ROOT.glob('*') for pth in _.glob('*.dist-info')] +_USER_EGG_INFOS = [pth for _ in _GIT_REPOS_ROOT.glob('*') for pth in _.glob('*.egg-info')] # print(_SYS_DIST_INFOS, _SYS_EGG_INFOS, _USER_DIST_INFOS, _USER_EGG_INFOS) METADATA_JSON_EXAMPLE_PATHS = list(itertools.chain.from_iterable( From e3720f7edae1bba79cf57a5ed8a023eb02ac6494 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 23:26:01 +0900 Subject: [PATCH 09/14] test(github): install jupyter as an example package --- .github/workflows/python.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 6c7db6a..acca7d8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,6 +29,7 @@ jobs: - run: cd ../argunparse && pip install -r test_requirements.txt && python setup.py build && cd - - run: git clone https://github.com/python-semver/python-semver ../semver - run: cd ../semver && python setup.py build && cd - + - run: pip install jupyter - run: python -m coverage run --branch --source . -m unittest -v # - run: LOGGING_LEVEL=critical python -m coverage run --append --branch --source . -m unittest -v test.test_version - run: python -m coverage report --show-missing From a97580cabf709fcf222fee49edffc71d96d33be4 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 17 Aug 2022 23:45:20 +0900 Subject: [PATCH 10/14] build(Jenkins): add Jenkinsfile --- Dockerfile | 76 +++++++++++++++++++++++++++++++++ Jenkinsfile | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 Dockerfile create mode 100644 Jenkinsfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ed5825f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +ARG PYTHON_VERSION="3.10" + +FROM python:${PYTHON_VERSION} + +SHELL ["/bin/bash", "-c"] + +# set timezone + +ARG TIMEZONE="Europe/Warsaw" + +RUN set -Eeuxo pipefail && \ + apt-get update && \ + apt-get install --no-install-recommends -y \ + tzdata && \ + echo "${TIMEZONE}" > /etc/timezone && \ + cp "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime && \ + apt-get -qy autoremove && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# add a non-root user + +ARG USER_ID=1000 +ARG GROUP_ID=1000 +ARG AUX_GROUP_IDS="" + +RUN set -Eeuxo pipefail && \ + addgroup --gid "${GROUP_ID}" user && \ + adduser --disabled-password --gecos "User" --uid "${USER_ID}" --gid "${GROUP_ID}" user && \ + echo ${AUX_GROUP_IDS} | xargs -n1 echo | xargs -I% addgroup --gid % group% && \ + echo ${AUX_GROUP_IDS} | xargs -n1 echo | xargs -I% usermod --append --groups group% user + +# install dependencies + +RUN set -Eeuxo pipefail && \ + apt-get update && \ + apt-get install --no-install-recommends -y \ + git && \ + apt-get -qy autoremove && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# prepare version_query for testing + +WORKDIR /home/user/version-query + +COPY --chown=${USER_ID}:${GROUP_ID} requirements*.txt ./ + +RUN set -Eeuxo pipefail && \ + pip3 install -r requirements_ci.txt + +USER user + +WORKDIR /home/user + +VOLUME ["/home/user/version-query"] + +ENV EXAMPLE_PROJECTS_PATH="/home/user" + +RUN set -Eeuxo pipefail && \ + git clone https://github.com/PyCQA/pycodestyle ../pycodestyle && \ + cd ../pycodestyle && \ + python setup.py build && \ + cd - && \ + git clone https://github.com/mbdevpl/argunparse ../argunparse && \ + cd ../argunparse && \ + pip install -r test_requirements.txt && \ + python setup.py build && \ + cd - && \ + git clone https://github.com/python-semver/python-semver ../semver && \ + cd ../semver && \ + python setup.py build && \ + cd - && \ + pip install jupyter + +WORKDIR /home/user/version-query diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..e989591 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,119 @@ +#!/usr/bin/env groovy + +library 'jenkins-mbdev-pl-libs' + +pipeline { + + options { + ansiColor('xterm') + } + + environment { + PYTHON_MODULES = 'ingit test *.py' + } + + agent any + + stages { + stage('Matrix') { + matrix { + + axes { + axis { + name 'PYTHON_VERSION' + values '3.8', '3.9', '3.10' + } + } + + agent { + dockerfile { + additionalBuildArgs '--build-arg USER_ID=${USER_ID} --build-arg GROUP_ID=${GROUP_ID}' \ + + ' --build-arg AUX_GROUP_IDS="${AUX_GROUP_IDS}" --build-arg TIMEZONE=${TIMEZONE}' \ + + ' --build-arg PYTHON_VERSION=${PYTHON_VERSION}' + label 'docker' + } + } + + stages { + + stage('Lint') { + when { + environment name: 'PYTHON_VERSION', value: '3.10' + } + steps { + sh """#!/usr/bin/env bash + set -Eeux pipefail + python -m pylint ${PYTHON_MODULES} |& tee pylint.log + echo "\${PIPESTATUS[0]}" | tee pylint_status.log + python -m mypy ${PYTHON_MODULES} |& tee mypy.log + echo "\${PIPESTATUS[0]}" | tee mypy_status.log + python -m pycodestyle ${PYTHON_MODULES} |& tee pycodestyle.log + echo "\${PIPESTATUS[0]}" | tee pycodestyle_status.log + python -m pydocstyle ${PYTHON_MODULES} |& tee pydocstyle.log + echo "\${PIPESTATUS[0]}" | tee pydocstyle_status.log + """ + } + } + + stage('Test') { + steps { + sh '''#!/usr/bin/env bash + set -Eeuxo pipefail + TEST_PACKAGING=1 python -m coverage run --branch --source . -m unittest -v + ''' + } + } + + stage('Coverage') { + when { + environment name: 'PYTHON_VERSION', value: '3.10' + } + steps { + sh '''#!/usr/bin/env bash + set -Eeux + python -m coverage report --show-missing |& tee coverage.log + echo "${PIPESTATUS[0]}" | tee coverage_status.log + ''' + script { + defaultHandlers.afterPythonBuild() + } + } + } + + stage('Codecov') { + environment { + CODECOV_TOKEN = credentials('codecov-token-mbdevpl-version-query') + } + steps { + sh '''#!/usr/bin/env bash + set -Eeuxo pipefail + python -m codecov --token ${CODECOV_TOKEN} + ''' + } + } + + } + + } + } + } + + post { + unsuccessful { + script { + defaultHandlers.afterBuildFailed() + } + } + regression { + script { + defaultHandlers.afterBuildBroken() + } + } + fixed { + script { + defaultHandlers.afterBuildFixed() + } + } + } + +} From 5c11406a61634902703943e78ba697dde5b5ce25 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 18 Aug 2022 00:44:31 +0900 Subject: [PATCH 11/14] build(jenkins): use correct paths --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index ed5825f..c7fa17d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,17 +58,17 @@ VOLUME ["/home/user/version-query"] ENV EXAMPLE_PROJECTS_PATH="/home/user" RUN set -Eeuxo pipefail && \ - git clone https://github.com/PyCQA/pycodestyle ../pycodestyle && \ - cd ../pycodestyle && \ + git clone https://github.com/PyCQA/pycodestyle pycodestyle && \ + cd pycodestyle && \ python setup.py build && \ cd - && \ - git clone https://github.com/mbdevpl/argunparse ../argunparse && \ - cd ../argunparse && \ + git clone https://github.com/mbdevpl/argunparse argunparse && \ + cd argunparse && \ pip install -r test_requirements.txt && \ python setup.py build && \ cd - && \ - git clone https://github.com/python-semver/python-semver ../semver && \ - cd ../semver && \ + git clone https://github.com/python-semver/python-semver semver && \ + cd semver && \ python setup.py build && \ cd - && \ pip install jupyter From a1f4ce3037f539c0b89cbcd3157539d1021cc4db Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 18 Aug 2022 00:45:11 +0900 Subject: [PATCH 12/14] chore: fix a cosmetic issue --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b1c5a5e..1e094d7 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ Zero-overhead package versioning for Python. :target: https://github.com/mbdevpl/version-query/actions :alt: build status from GitHub -.. image:: https://codecov.io/gh/mbdevpl/version-query/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/mbdevpl/version-query/branch/main/graph/badge.svg :target: https://codecov.io/gh/mbdevpl/version-query :alt: test coverage from Codecov From 8a1824ede7bbb800bdf897ccf619479ccd1f38a5 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 18 Aug 2022 01:19:41 +0900 Subject: [PATCH 13/14] build(jenkins): use correct package name --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e989591..344d5b5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { } environment { - PYTHON_MODULES = 'ingit test *.py' + PYTHON_MODULES = 'version_query test *.py' } agent any From 196dc8378b554a20cf17c54e3ad20547f00c42a2 Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Thu, 18 Aug 2022 01:20:06 +0900 Subject: [PATCH 14/14] chore: consistency updates --- .dockerignore | 30 ++++++ .gitignore | 6 ++ pyproject.toml | 26 +++++- setup_boilerplate.py | 215 +++++++++++++++++++++++++++---------------- test/test_setup.py | 66 ++++++------- 5 files changed, 232 insertions(+), 111 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a5c6c45 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +## /.gitignore + +# IDE: Visual Studio Code +/.vscode + +# OS: macOS +.DS_Store + +# OS: Windows +desktop.ini +System Volume Information +Thumbs.db +Thumbs.db* + +# Python +/build +/dist +/.cache +__pycache__ +*.egg +*.egg-info +*.pyc + +# Python: coverage +/htmlcov +/.coverage + +# Python: Jupyter notebooks +.ipynb_checkpoints +*-checkpoint.ipynb diff --git a/.gitignore b/.gitignore index 6e6578b..d91d2f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ +# IDE: Visual Studio Code +/.vscode + # OS: macOS .DS_Store # OS: Windows desktop.ini +System Volume Information +Thumbs.db +Thumbs.db* # Python /build diff --git a/pyproject.toml b/pyproject.toml index 3d0a46b..01ef51f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,26 @@ [build-system] -requires=['docutils', 'GitPython', 'packaging', 'semver', 'setuptools', 'wheel'] +requires = ['docutils', 'GitPython', 'packaging', 'semver', 'setuptools', 'wheel'] + +[tool.pydocstyle] +ignore = ['D102', 'D103', 'D105', 'D107', 'D203', 'D213', 'D406', 'D407', 'D412', 'D413'] + +[tool.pylint.MASTER] +load-plugins = [ + 'pylint.extensions.mccabe', + 'pylint.extensions.redefined_variable_type', + 'pylint.extensions.broad_try_clause' +] + +[tool.pylint.'MESSAGES CONTROL'] +docstring-min-length = 5 + +[tool.pylint.SIMILARITIES] +ignore-imports = 'yes' +min-similarity-lines = 5 + +[tool.pylint.BASIC] +no-docstring-rgx = '^(test)?_|.*Tests$' +unsafe-load-any-extension = 'yes' + +[tool.pylint.REPORTS] +output-format = 'colorized' diff --git a/setup_boilerplate.py b/setup_boilerplate.py index 8babe87..ead8617 100644 --- a/setup_boilerplate.py +++ b/setup_boilerplate.py @@ -1,54 +1,60 @@ """Below code is generic boilerplate and normally should not be changed. -To avoid setup script boilerplate, create "setup.py" file with the minimal contents as given -in SETUP_TEMPLATE below, and modify it according to the specifics of your package. - -See the implementation of setup_boilerplate.Package for default metadata values and available -options. -""" - -import logging -import pathlib -import runpy -import sys -import typing as t - -import docutils.nodes -import docutils.parsers.rst -import docutils.utils -import setuptools +See the implementation of setup_boilerplate.Package for available options. +Also, some fields have default values. See DEFAULT_* constants below for those values. -__updated__ = '2020-02-05' - -_LOG = logging.getLogger(__name__) +To avoid setup script boilerplate, create "setup.py" file with the minimal contents as given +below and modify it according to the specifics of your package. +Note: string limiters need to be adjusted. -SETUP_TEMPLATE = '''"""Setup script.""" +"" "Setup script." "" import setup_boilerplate class Package(setup_boilerplate.Package): - - """Package metadata.""" + "" "Package metadata." "" name = '' description = '' url = 'https://github.com/mbdevpl/...' classifiers = [ 'Development Status :: 1 - Planning', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only'] keywords = [] if __name__ == '__main__': Package.setup() -''' +""" + +import logging +import pathlib +import runpy +import sys +import typing as t + +import docutils.frontend +import docutils.nodes +import docutils.parsers.rst +import docutils.utils +import setuptools + +__version__ = '2022.01.03' + +_LOG = logging.getLogger(__name__) HERE = pathlib.Path(__file__).resolve().parent +DEFAULT_URL = 'https://github.com/mbdevpl' +DEFAULT_AUTHOR = 'Mateusz Bysiek' +DEFAULT_AUTHOR_EMAIL = 'mateusz.bysiek@gmail.com' +DEFAULT_LICENSE_STR = 'Apache License 2.0' + def find_version( package_name: str, version_module_name: str = '_version', @@ -57,6 +63,11 @@ def find_version( To avoid importing whole package only to read the version, just module containing the version is imported. Therefore relative imports in that module will break the setup. + + :param package_name: name of the package + :param version_module_name: name of the module containing the version + :param version_variable_name: name of the variable containing the version + :return: version string """ version_module_path = f'{package_name.replace("-", "_")}/{version_module_name}.py' version_module_vars = runpy.run_path(version_module_path) @@ -64,7 +75,11 @@ def find_version( def find_packages(root_directory: str = '.') -> t.List[str]: - """Find packages to pack.""" + """Find packages to pack. + + :param root_directory: directory to start searching from + :return: list of packages + """ exclude = ['test', 'test.*'] if ('bdist_wheel' in sys.argv or 'bdist' in sys.argv) else [] packages_list = setuptools.find_packages(root_directory, exclude=exclude) return packages_list @@ -75,10 +90,13 @@ def parse_requirements( """Read contents of requirements.txt file and return data from its relevant lines. Only non-empty and non-comment lines are relevant. + + :param requirements_path: path to the requirements file if custom one is used + :return: list of requirements """ requirements = [] - with HERE.joinpath(requirements_path).open() as reqs_file: - for requirement in [line.strip() for line in reqs_file.read().splitlines()]: + with HERE.joinpath(requirements_path).open(encoding='utf-8') as requirements_file: + for requirement in [line.strip() for line in requirements_file.read().splitlines()]: if not requirement or requirement.startswith('#'): continue requirements.append(requirement) @@ -87,10 +105,21 @@ def parse_requirements( def partition_version_classifiers( classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ', - only_suffix: str = ' :: Only') -> t.Tuple[t.List[t.Sequence[int]], t.List[t.Sequence[int]]]: - """Find version number classifiers in given list and partition them into 2 groups.""" - versions_min: t.List[t.Sequence[int]] = [] - versions_only: t.List[t.Sequence[int]] = [] + only_suffix: str = ' :: Only' + ) -> t.Tuple[t.List[t.Tuple[int, ...]], t.List[t.Tuple[int, ...]]]: + """Find version number classifiers in given list and partition them into 2 groups. + + The 2 groups being: + 1. minimum versions, any version at least as high this is compatible + 2. only versions, this specific version is compatible, but nothing is said about other versions + + :param classifiers: sequence of trove classifiers to be analysed + :param version_prefix: prefix that identifies a version classifier + :param only_suffix: prefix that identifies an "only version" classifier + :return: parsed and partitioned version tuples + """ + versions_min: t.List[t.Tuple[int, ...]] = [] + versions_only: t.List[t.Tuple[int, ...]] = [] for classifier in classifiers: version = classifier.replace(version_prefix, '') versions = versions_min @@ -98,7 +127,7 @@ def partition_version_classifiers( version = version.replace(only_suffix, '') versions = versions_only try: - versions.append(tuple([int(_) for _ in version.split('.')])) + versions.append(tuple(int(_) for _ in version.split('.'))) except ValueError: pass return versions_min, versions_only @@ -107,7 +136,13 @@ def partition_version_classifiers( def find_required_python_version( classifiers: t.Sequence[str], version_prefix: str = 'Programming Language :: Python :: ', only_suffix: str = ' :: Only') -> t.Optional[str]: - """Determine the minimum required Python version.""" + """Determine the minimum required Python version. + + :param classifiers: sequence of trove classifiers to be analysed + :param version_prefix: prefix that identifies a version classifier + :param only_suffix: prefix that identifies an "only version" classifier + :return: minimum required Python version string, if any + """ versions_min, versions_only = partition_version_classifiers( classifiers, version_prefix, only_suffix) if len(versions_only) > 1: @@ -156,15 +191,15 @@ def visit_reference(self, node: docutils.nodes.reference) -> None: assert isinstance(node, docutils.nodes.TextElement), type(node) _LOG.debug('RelativeRefFinder: examining reference %s', node) if len(node.children) != 1 or 'refuri' not in node.attributes \ - or any(node.attributes['refuri'].startswith(_) for _ in {'http://', 'https://'}): + or node.attributes['refuri'].startswith(('http://', 'https://')): return - # print(' RelativeRefFinder: reference passed initial check') + # _LOG.debug(' RelativeRefFinder: reference passed initial check') path = pathlib.Path(node.attributes['refuri']) + if path.is_absolute(): + return try: - if path.is_absolute(): - return - resolved_path = path.resolve() - except OSError: # in is_absolute() and resolve(), on URLs in Windows + resolved_path = path.resolve(strict=True) + except FileNotFoundError: return try: resolved_path.relative_to(self.root_dir) @@ -185,9 +220,12 @@ def resolve_relative_rst_links(text: str, base_link: str) -> str: Links are resolved only if they point to files existing in the project's working directory. - All links of form `link`_ become `link `_. + All links of form `link`_ become `link `_. - And all + And all link of the form :target: link become :target: absolute_link. + + Where absolute_link is made by concatenating base_link to link with no separator added + in between. So in most cases base_link should and with a slash. """ document = parse_rst(text) finder = RelativeRefFinder(HERE, document) @@ -214,61 +252,80 @@ def resolve_relative_rst_links(text: str, base_link: str) -> str: class Package: """Default metadata and behaviour for a Python package setup script.""" - root_directory = '.' # type: str + root_directory: str = '.' """Root directory of the source code of the package, relative to the setup.py file location.""" - name = None # type: str + name: str + """Package name.""" - version = None # type: str - """"If None, it will be obtained from "package_name._version.VERSION" variable.""" + version: str + """Package version as string. - description = None # type: str + If None, it will be obtained from "package_name._version.VERSION" variable. + """ - long_description = None # type: str - """If None, it will be generated from readme.""" + description: str + """One line of text that shortly describes the package.""" - long_description_content_type = None # type: str - """If None, it will be set accodring to readme file extension. + long_description: str + """Full description of the package, can be arbitrarily long. - For this field to be automatically set, also long_description field has to be None. + If not set, it will be generated from readme. """ - url = 'https://github.com/mbdevpl' # type: str - download_url = None # type: str - author = 'Mateusz Bysiek' # type: str - author_email = 'mateusz.bysiek@gmail.com' # type: str - # maintainer = None # type: str - # maintainer_email = None # type: str - license_str = 'Apache License 2.0' # type: str + long_description_content_type: str + """Content type of the long description. + + Usually one of 'text/x-rst', 'text/markdown' or 'text/plain'. + + If not set, it will be set according to readme file extension. + For this field to be automatically set, also long_description field cannot be set. + """ - classifiers = [] # type: t.List[str] - """List of valid project classifiers: https://pypi.org/pypi?:action=list_classifiers""" + url: str = DEFAULT_URL + download_url: str = '' + author: str = DEFAULT_AUTHOR + author_email: str = DEFAULT_AUTHOR_EMAIL + maintainer: str + maintainer_email: str + license_str: str = DEFAULT_LICENSE_STR - keywords = [] # type: t.List[str] + classifiers: t.List[str] = [] + """List of trove classifiers for the package. - packages = None # type: t.List[str] - """If None, determined with help of setuptools.""" + List of valid classifiers: https://pypi.org/pypi?:action=list_classifiers + """ + + keywords: t.List[str] = [] + + packages: t.List[str] + """List of Python packages to be included in the distributed file. + + Usually it's just one package per distributed file, but any number is supported. + + If not set, determined with help of setuptools. + """ - package_data = {} # type: t.Dict[str, t.List[str]] - exclude_package_data = {} # type: t.Dict[str, t.List[str]] + package_data: t.Dict[str, t.List[str]] = {} + exclude_package_data: t.Dict[str, t.List[str]] = {} - install_requires = None # type: t.List[str] + install_requires: t.Optional[t.List[str]] = None """If None, determined using requirements.txt.""" - extras_require = {} # type: t.Mapping[str, t.List[str]] + extras_require: t.Mapping[str, t.List[str]] = {} """A dictionary containing entries of type 'some_feature': ['requirement1', 'requirement2'].""" - python_requires = None # type: str + python_requires: t.Optional[str] = None """If None, determined from provided classifiers.""" - entry_points = {} # type: t.Mapping[str, t.List[str]] + entry_points: t.Mapping[str, t.List[str]] = {} """A dictionary used to enable automatic creation of console scripts, gui scripts and plugins. Example entry: 'console_scripts': ['script_name = package.subpackage:function'] """ - test_suite = 'test' # type: str + test_suite: str = 'test' @classmethod def try_fields(cls, *names) -> t.Optional[t.Any]: @@ -287,7 +344,7 @@ def parse_readme(cls, readme_filename: str = 'README.rst', """ readme_path = HERE.joinpath(readme_filename) with readme_path.open(encoding=encoding) as readme_file: - long_description = readme_file.read() # type: str + long_description: str = readme_file.read() if readme_path.suffix.lower() == '.rst' and cls.url.startswith('https://github.com/'): base_url = f'{cls.url}/blob/v{cls.version}/' @@ -302,11 +359,15 @@ def parse_readme(cls, readme_filename: str = 'README.rst', @classmethod def prepare(cls) -> None: """Fill in possibly missing package metadata.""" - if cls.version is None: + if not hasattr(cls, 'version'): cls.version = find_version(cls.name) - if cls.long_description is None: + if not hasattr(cls, 'long_description'): cls.long_description, cls.long_description_content_type = cls.parse_readme() - if cls.packages is None: + if not hasattr(cls, 'maintainer'): + cls.maintainer = cls.author + if not hasattr(cls, 'maintainer_email'): + cls.maintainer_email = cls.author_email + if not hasattr(cls, 'packages'): cls.packages = find_packages(cls.root_directory) if cls.install_requires is None: cls.install_requires = parse_requirements() @@ -323,8 +384,8 @@ def setup(cls) -> None: long_description_content_type=cls.long_description_content_type, url=cls.url, download_url=cls.download_url, author=cls.author, author_email=cls.author_email, - maintainer=cls.try_fields('maintainer', 'author'), - maintainer_email=cls.try_fields('maintainer_email', 'author_email'), + maintainer=cls.maintainer, + maintainer_email=cls.maintainer_email, license=cls.license_str, classifiers=cls.classifiers, keywords=cls.keywords, packages=cls.packages, package_dir={'': cls.root_directory}, include_package_data=True, diff --git a/test/test_setup.py b/test/test_setup.py index 8efa9e8..3c9d266 100644 --- a/test/test_setup.py +++ b/test/test_setup.py @@ -12,27 +12,24 @@ import typing as t import unittest -__updated__ = '2020-02-05' +__version__ = '2022.08.15' -def run_program(*args, glob: bool = False): +def run_program(*args, glob: bool = False) -> None: """Run subprocess with given args. Use path globbing for each arg that contains an asterisk.""" if glob: cwd = pathlib.Path.cwd() args = tuple(itertools.chain.from_iterable( list(str(_.relative_to(cwd)) for _ in cwd.glob(arg)) if '*' in arg else [arg] for arg in args)) - process = subprocess.Popen(args) - process.wait() - if process.returncode != 0: - raise AssertionError(f'execution of {args} returned {process.returncode}') - return process + try: + subprocess.run(args, check=True) + except subprocess.CalledProcessError as err: + raise AssertionError(f'execution of {args} failed') from err -def run_pip(*args, **kwargs): - python_exec_name = pathlib.Path(sys.executable).name - pip_exec_name = python_exec_name.replace('python', 'pip') - run_program(pip_exec_name, *args, **kwargs) +def run_pip(*args, **kwargs) -> None: + run_program(sys.executable, '-m', 'pip', *args, **kwargs) def run_module(name: str, *args, run_name: str = '__main__') -> None: @@ -99,7 +96,7 @@ def import_module_member(module_name: str, member_name: str) -> t.Any: LINK_EXAMPLES = [ (None, 'setup.py', True), ('this file', 'setup.py', True), (None, 'test/test_setup.py', True), (None, 'http://site.com', False), (None, '../something/else', False), (None, 'no.thing', False), - (None, '/my/abs/path', False), (None, 'ftp://server.com', False)] + (None, '/my/abs/path', False)] def get_package_folder_name(): @@ -135,8 +132,8 @@ def test_requirements(self): def test_requirements_empty(self): parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements') - reqs_file = tempfile.NamedTemporaryFile('w', delete=False) - reqs_file.close() + with tempfile.NamedTemporaryFile('w', delete=False) as reqs_file: + pass results = parse_requirements(reqs_file.name) self.assertIsInstance(results, list) self.assertEqual(len(results), 0) @@ -145,10 +142,9 @@ def test_requirements_empty(self): def test_requirements_comments(self): parse_requirements = import_module_member('setup_boilerplate', 'parse_requirements') reqs = ['# comment', 'numpy', '', '# another comment', 'scipy', '', '# one more comment'] - reqs_file = tempfile.NamedTemporaryFile('w', delete=False) - for req in reqs: - print(req, file=reqs_file) - reqs_file.close() + with tempfile.NamedTemporaryFile('w', delete=False) as reqs_file: + for req in reqs: + print(req, file=reqs_file) results = parse_requirements(reqs_file.name) self.assertIsInstance(results, list) self.assertGreater(len(results), 0) @@ -214,7 +210,6 @@ def test_python_versions_conflict(self): class PackageTests(unittest.TestCase): - """Test methods of Package class.""" def test_try_fields(self): @@ -284,13 +279,13 @@ class Package(package): # pylint: disable=too-few-public-methods, missing-docst self.assertEqual(Package.version, version_) self.assertEqual(Package.long_description, long_description_) - Package.long_description = None - Package.packages = None - Package.install_requires = None - Package.python_requires = None + del Package.long_description + del Package.packages + del Package.install_requires + del Package.python_requires Package.prepare() - Package.version = None + del Package.version with self.assertRaises(FileNotFoundError): Package.prepare() @@ -298,7 +293,6 @@ class Package(package): # pylint: disable=too-few-public-methods, missing-docst @unittest.skipUnless(os.environ.get('TEST_PACKAGING') or os.environ.get('CI'), 'skipping packaging tests for actual package') class IntergrationTests(unittest.TestCase): - """Test if the boilerplate can actually create a valid package.""" pkg_name = get_package_folder_name() @@ -316,26 +310,32 @@ def test_build_source(self): self.assertTrue(os.path.isdir('dist')) def test_install_code(self): - run_pip('install', '.') - run_pip('uninstall', '-y', self.pkg_name) + with tempfile.TemporaryDirectory() as temporary_folder: + run_pip('install', '--prefix', temporary_folder, '.') + self.assertFalse(pathlib.Path(temporary_folder).exists()) def test_install_source_tar(self): find_version = import_module_member('setup_boilerplate', 'find_version') version = find_version(self.pkg_name) - run_pip('install', f'dist/*-{version}.tar.gz', glob=True) - run_pip('uninstall', '-y', self.pkg_name) + with tempfile.TemporaryDirectory() as temporary_folder: + run_pip( + 'install', '--prefix', temporary_folder, f'dist/*-{version}.tar.gz', glob=True) + self.assertFalse(pathlib.Path(temporary_folder).exists()) def test_install_source_zip(self): find_version = import_module_member('setup_boilerplate', 'find_version') version = find_version(self.pkg_name) - run_pip('install', f'dist/*-{version}.zip', glob=True) - run_pip('uninstall', '-y', self.pkg_name) + with tempfile.TemporaryDirectory() as temporary_folder: + run_pip('install', '--prefix', temporary_folder, f'dist/*-{version}.zip', glob=True) + self.assertFalse(pathlib.Path(temporary_folder).exists()) def test_install_wheel(self): find_version = import_module_member('setup_boilerplate', 'find_version') version = find_version(self.pkg_name) - run_pip('install', f'dist/*-{version}-*.whl', glob=True) - run_pip('uninstall', '-y', self.pkg_name) + with tempfile.TemporaryDirectory() as temporary_folder: + run_pip( + 'install', '--prefix', temporary_folder, f'dist/*-{version}-*.whl', glob=True) + self.assertFalse(pathlib.Path(temporary_folder).exists()) def test_pip_error(self): with self.assertRaises(AssertionError):