From 05627f176d23637a7acbd4492303d3d33f1b3ebc Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:41:31 +0100 Subject: [PATCH] Switch to `pyproject.toml` (#240) * Switch to `pyproject.toml * Add pytest-xdist as test dependency & improve GH Actions GH Actions: - Use more processes in xdist for speedup - Upload coverage to Codecov in separate job - Add build test - General * Support python 3. * Consolidate pytest & bandit configs into `pyproject.toml * Coverage: improve exclusions --- .bandit | 1 - .coveragerc | 18 +++++- .github/workflows/build.yml | 51 +++++++++++++++ .github/workflows/test.yml | 80 ++++++++++++++++------- README.md | 9 +-- metomi/isodatetime/__init__.py | 2 + pyproject.toml | 104 +++++++++++++++++++++++++++++ pytest.ini | 28 -------- setup.cfg | 24 ------- setup.py | 115 --------------------------------- 10 files changed, 233 insertions(+), 199 deletions(-) delete mode 100644 .bandit create mode 100644 .github/workflows/build.yml create mode 100644 pyproject.toml delete mode 100644 pytest.ini delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.bandit b/.bandit deleted file mode 100644 index 75d550c..0000000 --- a/.bandit +++ /dev/null @@ -1 +0,0 @@ -skips: ['B101'] diff --git a/.coveragerc b/.coveragerc index 32c66d9..5d9c68a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -27,10 +27,26 @@ source= [report] exclude_lines = pragma: no cover + def __repr__ - raise NotImplementedError + @(abc\.)?abstractmethod + + # Ignore code that can only run in CLI: if __name__ == .__main__.: + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + return NotImplemented + + # Ignore type checking code: + if (typing\.)?TYPE_CHECKING: + @overload( |$) + + # Don't complain about ellipsis (exception classes, typing overloads etc): + \.\.\. + def parse_args + omit= metomi/isodatetime/tests precision=2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6f9a319 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build + +# build the project whenever the configuration is changed + +on: + workflow_dispatch: + pull_request: + paths: + - 'README.md' # check markdown is valid + - 'MANIFEST.in' # check packaging + - 'pyproject.toml' # check build config + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 2 + +jobs: + build: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + matrix: + os: ['ubuntu-latest'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + include: + - os: 'macos-latest' + python: '3.7' + name: ${{ matrix.os }} py-${{ matrix.python }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Build + uses: cylc/release-actions/build-python-package@v1 + with: + check-dependencies: false + + - name: Inspect + run: | + unzip -l dist/*.whl | tee files + grep 'metomi/isodatetime/data.py' files + grep 'metomi_isodatetime.*.dist-info/LICENSE' files + # grep 'metomi/isodatetime/py.typed' files # (not yet added) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ecfa8b..b8e5e6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,31 +4,41 @@ on: pull_request: push: branches: [master] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 2 jobs: test: runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 40 strategy: matrix: os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] coverage: [false] include: # Modify existing configurations: - os: ubuntu-latest - python-version: '3.11' + python-version: '3.12' coverage: false tz: 'XXX-05:30' # UTC+05:30 # Add new configurations: - os: ubuntu-latest - python-version: '3.11' + python-version: '3.12' coverage: true - os: macos-latest - python-version: '3.11' + python-version: '3.12' coverage: false + name: ${{ matrix.os }} py-${{ matrix.python-version }} ${{ matrix.tz }} ${{ matrix.coverage && '(coverage)' || '' }} + env: + PYTEST_ADDOPTS: -n 5 -m 'slow or not slow' steps: - - name: Checkout repo uses: actions/checkout@v4 @@ -38,27 +48,53 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install - run: | - pip install --upgrade pip setuptools - pip install -e .[all] - pip install pytest-xdist + run: pip install -e .[all] - - name: Style test - run: flake8 metomi/isodatetime + - name: Style + if: ${{ !matrix.coverage }} + run: flake8 + + - name: Bandit + if: ${{ !matrix.coverage }} + run: bandit -r metomi/isodatetime -c pyproject.toml - name: Run tests env: TZ: ${{ matrix.tz }} - run: | - PYTEST_ARGS=(-n 2 -m 'slow or not slow') - if ${{ matrix.coverage }}; then - PYTEST_ARGS+=('--cov=metomi/isodatetime') - fi + PYTEST_ADDOPTS: ${{ matrix.coverage && format('{0} --cov=metomi/isodatetime', env.PYTEST_ADDOPTS) || env.PYTEST_ADDOPTS }} + run: pytest - pytest "${PYTEST_ARGS[@]}" - - - name: Upload coverage report + - name: Coverage report if: matrix.coverage run: | - coverage xml --ignore-errors - bash <(curl -s https://codecov.io/bash) + coverage xml + coverage report + + - name: Upload coverage artifact + if: matrix.coverage + uses: actions/upload-artifact@v3 + with: + name: coverage_${{ matrix.os }}_py-${{ matrix.python-version }} + path: coverage.xml + retention-days: 4 + + codecov-upload: + needs: test + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download coverage artifacts + uses: actions/download-artifact@v3 + + - name: Codecov upload + uses: codecov/codecov-action@v3 + with: + name: ${{ github.workflow }} + fail_ci_if_error: true + verbose: true + # Token not required for public repos, but avoids upload failure due + # to rate-limiting (but not for PRs opened from forks) + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 2777be4..6b5477c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ isodatetime [![Test](https://github.com/metomi/isodatetime/workflows/Test/badge.svg?event=push)](https://github.com/metomi/isodatetime/actions?query=workflow%3ATest) [![codecov](https://codecov.io/gh/metomi/isodatetime/branch/master/graph/badge.svg)](https://codecov.io/gh/metomi/isodatetime) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.597555.svg)](https://doi.org/10.5281/zenodo.597555) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1fd1147b75474d4d9a0f64bececf3bb5)](https://www.codacy.com/app/metomi/isodatetime?utm_source=github.com&utm_medium=referral&utm_content=metomi/isodatetime&utm_campaign=Badge_Grade) [![PYPI Badge](https://img.shields.io/pypi/v/metomi-isodatetime)](https://pypi.org/project/metomi-isodatetime/) Python [ISO8601 (2004)](https://www.iso.org/standard/40874.html) @@ -19,13 +18,7 @@ Install from PyPI: $ pip install metomi-isodatetime ``` -Or build yourself: - -```console -$ git clone https://github.com/metomi/isodatetime.git isodatetime -$ cd isodatetime -$ python setup.py install -``` +Or with conda: see https://github.com/conda-forge/metomi-isodatetime-feedstock ## Usage diff --git a/metomi/isodatetime/__init__.py b/metomi/isodatetime/__init__.py index 8e101bc..7379be8 100644 --- a/metomi/isodatetime/__init__.py +++ b/metomi/isodatetime/__init__.py @@ -18,3 +18,5 @@ """Python ISO 8601 date time parser and data model/manipulation utilities.""" __version__ = "3.1.0" + +FULL_VERSION = f"1!{__version__}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ec58d08 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,104 @@ +# Copyright (C) British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +[build-system] +requires = ["setuptools >= 49.0"] +build-backend = "setuptools.build_meta" + + +[project] +name = "metomi-isodatetime" +authors = [ + {name = "Met Office", email = "metomi@metoffice.gov.uk"} +] +description = "Python ISO 8601 date time parser and data model/manipulation utilities" +license = {text = "LGPLv3"} +readme = "README.md" +keywords = ["isodatetime", "datetime", "iso8601", "date", "time", "parser"] +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities" +] +dynamic = ["version"] + + +[project.urls] +Homepage = "https://github.com/metomi/isodatetime" +Repository = "https://github.com/metomi/isodatetime" +Changelog = "https://github.com/metomi/isodatetime/blob/master/CHANGES.md" + + +[tool.setuptools] +platforms = ["any"] + + +[tool.setuptools.dynamic] +version = {attr = "metomi.isodatetime.FULL_VERSION"} + + +[tool.setuptools.packages.find] +include = ["metomi*"] +exclude = ["metomi.isodatetime.tests*"] + + +[tool.setuptools.package-data] +"metomi.isodatetime" = ["py.typed"] + + +[project.scripts] +isodatetime = "metomi.isodatetime.main:main" + + +[project.optional-dependencies] +test = [ + "coverage", + "pytest>=6", + "pytest-env", + "pytest-cov", + "pytest-xdist", + "flake8", + "bandit>=1.7.1", +] +all = [ + "metomi-isodatetime[test]", +] + + +[tool.pytest.ini_options] +addopts = "-v -s -ra --color=auto --doctest-glob='*.md' -m 'not slow'" +markers = [ + "slow: mark a test as slow - it will be skipped by default (use '-m \"slow or not slow\"' to run all tests)" +] +testpaths = [ + "metomi/isodatetime/tests", + "README.md", +] + + +[tool.bandit] +exclude_dirs = ["metomi/isodatetime/tests"] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 3629552..0000000 --- a/pytest.ini +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (C) British Crown (Met Office) & Contributors. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - -[pytest] -addopts = - -v - -s - -ra - --color=auto - --doctest-glob='*.md' - -m 'not slow' -markers = - slow: mark a test as slow - it will be skipped by default (use '-m "slow or not slow"' to run all tests) -testpaths = - metomi/isodatetime/tests - README.md diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dff6bae..0000000 --- a/setup.cfg +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright (C) British Crown (Met Office) & Contributors. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# ---------------------------------------------------------------------------- - -[aliases] -# so that running python setup.py test invokes pytest -test=pytest - -[metadata] -license_files = LICENSE diff --git a/setup.py b/setup.py deleted file mode 100644 index c97eece..0000000 --- a/setup.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -# ---------------------------------------------------------------------------- -# Copyright (C) British Crown (Met Office) & Contributors. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# ---------------------------------------------------------------------------- - -from importlib.util import spec_from_file_location, module_from_spec -from pathlib import Path -# overriding setuptools command -# https://stackoverflow.com/a/51294311 -from setuptools.command.bdist_rpm import bdist_rpm as bdist_rpm_original -# to parse pytest command arguments -# https://docs.pytest.org/en/2.8.7/goodpractices.html#manual-integration -from setuptools import setup, find_namespace_packages - - -DIST_DIR = Path(__file__).resolve().parent - - -def get_version(module, path): - """Return the __version__ attr from a module sourced by FS path.""" - spec = spec_from_file_location(module, path) - module = module_from_spec(spec) - spec.loader.exec_module(module) - return module.__version__ - - -__version__ = get_version( - 'metomi.isodatetime', - str(Path(DIST_DIR, 'metomi/isodatetime/__init__.py')) -) - - -class bdist_rpm(bdist_rpm_original): - - def run(self): - """Before calling the original run method, let's change the - distribution name to create an RPM for python-isodatetime.""" - self.distribution.metadata.name = "python-isodatetime" - self.distribution.metadata.version = __version__ - super().run() - - -install_requires = [] -tests_require = [ - 'coverage', - 'pytest>=5', - 'pytest-env', - 'pytest-cov', - 'flake8' -] -extras_require = {} -extras_require['all'] = ( - tests_require - + list({ - req - for reqs in extras_require.values() - for req in reqs - }) -) - - -setup( - name="metomi-isodatetime", - version='1!' + __version__, - author="Met Office", - author_email="metomi@metoffice.gov.uk", - cmdclass={ - "bdist_rpm": bdist_rpm - }, - description=("Python ISO 8601 date time parser" + - " and data model/manipulation utilities"), - license="LGPLv3", - keywords="isodatetime datetime iso8601 date time parser", - url="https://github.com/metomi/isodatetime", - packages=find_namespace_packages(include=['metomi.*']), - long_description=open(str(Path(DIST_DIR, 'README.md')), 'r').read(), - long_description_content_type="text/markdown", - platforms='any', - install_requires=install_requires, - tests_require=tests_require, - extras_require=extras_require, - python_requires='>=3.7', - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Other Environment", - "Intended Audience :: Developers", - ("License :: OSI Approved" + - " :: GNU Lesser General Public License v3 (LGPLv3)"), - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Utilities" - ], - entry_points={ - 'console_scripts': ['isodatetime=metomi.isodatetime.main:main'], - }, -)