diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..9be9a41 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,103 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + pull_request: + push: + branches: + - main + tags: + - "v*" + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] #, windows-latest, macos-latest] + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "pypy-3.9" + + name: ${{ matrix.os }} - ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v4 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.5.22" + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install tox tox-uv + - name: Run tox + # Run tox using the version of Python in `PATH` + run: tox -e py + + dist: + runs-on: ubuntu-latest + needs: [test] + name: Build Python packages + steps: + - uses: actions/checkout@v4 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade wheel setuptools build + - name: Build package + run: python -m build -s -w -o dist/ + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist + + dist_check: + runs-on: ubuntu-latest + needs: [dist] + name: Twine check + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + - name: Install dependencies + run: pip install twine + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - run: twine check dist/* + + dist_upload: + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + needs: [dist_check] + name: PyPI upload + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 15201ac..b506588 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# Generated by setuptools_scm +package/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1a40c8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: "meta" + hooks: + - id: "check-hooks-apply" + - id: "check-useless-excludes" + + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: "v5.0.0" + hooks: + - id: "check-added-large-files" + - id: "check-merge-conflict" + - id: "check-yaml" + + - repo: "https://github.com/astral-sh/ruff-pre-commit" + rev: "v0.9.2" + hooks: + - id: "ruff" + args: ["--fix"] + + - repo: "https://github.com/psf/black-pre-commit-mirror" + rev: "24.10.0" + hooks: + - id: "black" + language_version: "python3.11" diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..70797e0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +# Add files to be included/excluded in the source distribution +#include /path/to/file +#recursive-include directory *.csv diff --git a/README.md b/README.md index 4262b63..1c2799e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ # python-project-starter + This is a template repo to act as a reference when starting up a new project in python. It consolidates best practices in regards to minimal level of documentation as well as the CI aspects. + +Local installation can be done using [`uv`](https://github.com/astral-sh/uv): + +```bash +$ uv venv -p python3.10 +$ uv pip install -e . +$ source .venv/bin/activate +$ python +>>> from package import square +>>> square(3) +9 +``` + +After installation a command-line tool is also available: + +```bash +$ square 4 +Square of 4 is 16 +``` + +Running the tests can be done using [`tox`](https://tox.wiki/): + +```bash +$ tox -p +``` + +Building the packages can also be done using `tox`: + +```bash +$ tox -e packages +$ ls dist/ +``` + +Packaging uses [`setuptools-scm`](https://github.com/pypa/setuptools-scm), so the version of the software is based on git tags. + +To run the linting, we recommend `ruff`, a standard configuration is in the repo in `pyproject.toml`. diff --git a/package/__init__.py b/package/__init__.py new file mode 100644 index 0000000..daf2215 --- /dev/null +++ b/package/__init__.py @@ -0,0 +1,2 @@ +from .module import * # noqa +from ._version import __version__ # noqa diff --git a/package/__main__.py b/package/__main__.py new file mode 100644 index 0000000..176221e --- /dev/null +++ b/package/__main__.py @@ -0,0 +1,23 @@ +from .module import square +from ._version import __version__ + + +def main() -> int: + import argparse + + parser = argparse.ArgumentParser(prog="Squarer of ints") + parser.add_argument("x", type=int) + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s, version {__version__}", + ) + args = parser.parse_args() + print(f"Square of {args.x} is {square(args.x)}") + return 0 + + +if __name__ == "__main__": + import sys + + sys.exit(main()) diff --git a/package/module.py b/package/module.py new file mode 100644 index 0000000..93dd803 --- /dev/null +++ b/package/module.py @@ -0,0 +1,7 @@ +__all__ = [ + "square", +] + + +def square(x: int) -> int: + return x**2 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..330a33f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +requires = ["setuptools>=71", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "package/_version.py" +write_to_template = "__version__ = \"{version}\"\n" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--strict-markers --doctest-modules --doctest-glob='*.rst' --ignore=setup.py" +doctest_optionflags = [ + "NORMALIZE_WHITESPACE", + "IGNORE_EXCEPTION_DETAIL", + "ELLIPSIS", +] +norecursedirs = [ + ".*", + "__pycache__", + "build", + "dist", + "*.egg-info", + "htmlcov", + "doc", +] +filterwarnings = [] + +[tool.ruff] +line-length = 100 +target-version = "py39" +extend-exclude = ["doc"] + +[tool.ruff.lint] +select = [ + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle + "UP", # pyupgrade + "YTT", # flake8-2020 + "B", # flake8-bugbear + "T10", # flake8-debugger + "C4", # flake8-comprehensions + "G", # flake8-logging-format + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions +] +ignore = [] + +[tool.black] +line-length = 88 +target-version = ["py39"] +extend-exclude = "doc" + +[tool.mypy] +warn_return_any = "True" +warn_unused_configs = "True" +strict_optional = "True" +ignore_missing_imports = "True" +disallow_any_unimported = "True" +check_untyped_defs = "True" +disallow_untyped_defs = "True" +no_implicit_optional = "True" +show_error_codes = "True" +warn_unused_ignores = "True" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1d7526b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[metadata] +name = Squarer +description = A package that squares integers +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/AmadeusITGroup/python-project-starter +author = your_name_here +author_email = no_reply@github.com +license_files = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >=3.9 +setup_requires = + setuptools_scm + +[options.entry_points] +console_scripts = + square = package.__main__:main diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4ca6776 --- /dev/null +++ b/setup.py @@ -0,0 +1,5 @@ +from setuptools import setup + +setup( + use_scm_version=True, +) diff --git a/test_package.py b/test_package.py new file mode 100644 index 0000000..6531dd1 --- /dev/null +++ b/test_package.py @@ -0,0 +1,10 @@ +import pytest +from package import square + + +@pytest.mark.parametrize( + "x,res", + [(1, 1), (2, 4), (-1, 1), (0, 0)], +) +def test_square(x: int, res: int) -> None: + assert square(x) == res diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..945ef51 --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +isolated_build = true +skip_missing_interpreters = true +envlist = + py{39,310,311,312,313} + pypy{39,310} + +[testenv] +package = wheel +wheel_build_env = .pkg +deps = pytest>=8.0 +commands = py.test {posargs} + +[testenv:ruff] +basepython = python3.9 +deps = ruff +commands = ruff check . + +[testenv:black] +basepython = python3.9 +deps = black +commands = black . + +[testenv:packages] +allowlist_externals = + rm +basepython = python3.9 +deps = + build + twine +commands = + rm -rf build *.egg-info + python -m build -s -w -o dist + twine check dist/*