Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: psf/black@stable
- uses: isort/isort-action@v1
lint:
name: Lint with ruff
runs-on: ubuntu-latest
Expand Down Expand Up @@ -106,6 +107,8 @@ jobs:
publish:
name: Publish package
if: startsWith(github.ref, 'refs/tags')
permissions:
id-token: write
needs:
- format
- lint
Expand Down
12 changes: 12 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
authors:
- family-names: "Last"
given-names: "First"
orcid: "https://orcid.org/0000-0000-0000-0000"
title: "Python Package Template repository"
version: 0.0.1
doi: 10.5281/zenodo.1234
date-released: 2025-07-23
url: "https://github.com/biosustain/python_package"
36 changes: 36 additions & 0 deletions Contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Contributing code

Install the code with development dependencies:

```bash
pip install -e '.[dev]'
```

## Format code and sort imports

```bash
black .
isort .
```

## lint code

```bash
ruff check .
```

## Run tests

```bash
pytest
```

## Sync notebooks with jupytext

For easier diffs, you can use jupytext to sync notebooks in the `docs/tutorial` directory with the percent format.

```bash
jupytext --sync docs/tutorial/*.ipynb
```

This is configured in the [`.jupytext`](docs/tutorial/.jupytext) file in that directory.
696 changes: 674 additions & 22 deletions LICENSE

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ see [GitHub documentation](https://docs.github.com/en/repositories/creating-and-
You will need to find and replace occurences of

- `python_package` -> `your_package_name`
- also the folder `src/python_package`
- also the folder `src/python_package`
- `RasmussenLab` -> `GitHub_user_name` (or `organization`)
with the name of your package and GitHub user name (or organization).
with the name of your package and GitHub user name (or organization).

- look for `First Last` to see where to replace with your name
- choose a license, see [GitHub documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/licensing-a-repository)
and [Creative Commons](https://creativecommons.org/chooser/).
Replace [`LICENSE`](LICENSE) file with the license you choose.
- Update the `CITATION.cff` file with your information.

## Development environment

Expand All @@ -41,7 +42,7 @@ print(hello_world(4))
## Readthedocs

The documentation can be build using readthedocs automatically. See
[project on Readthedocs](https://readthedocs.org/projects/rasmussenlab-python-package/)
[project on Readthedocs](https://readthedocs.org/projects/rasmussenlab-python-package/)
for the project based on this template. A new project needs
to [be registered on ReadTheDocs](https://docs.readthedocs.com/platform/stable/intro/add-project.html).

Expand Down
22 changes: 22 additions & 0 deletions developing.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ Please also update the project URL to your project:
"Homepage" = "https://github.com/RasmussenLab/python_package"
```

The template also sets a command line script entry point, which allows to run
the function `main` in the `mockup` module of the package as a command line script,
wrapping the `hello_world` function from the `mockup` module.
The template uses the standard library [argparse](https://docs.python.org/3/library/argparse.html)
module to parse parameters from the command line and creates a basic interface.

```toml
# Script entry points, i.e. command line commands available after installing the package
# e.g. implemented using argparse
# Then you can type: `python-package-hello -h` in the terminal
[project.scripts]
python-package-hello = "python_package.cli:main"
```

You can therefore run the command line script using:

```bash
python-package-hello -h
# print hello world 3 times
python-package-hello -n 3
```

## Source directory layout of the package

The source code of the package is located in the `src` directory, to have a project
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
html_js_files = [
"https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js"
]
os.environ["PLOTLY_RENDERER"] = "notebook" # compatibility with plotly6

# https://myst-nb.readthedocs.io/en/latest/configuration.html
# Execution
Expand Down
6 changes: 5 additions & 1 deletion docs/tutorial/.jupytext
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# all notebooks in this directory are in the percent format
# all notebooks in this directory are synced percent format when typing
# (jupytext is a dev dependency)
# jupytext --sync *.ipynb
# or from root directory
# jupytext --sync docs/api_examples/*.ipynb
formats = "ipynb,py:percent"
25 changes: 15 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# ref: https://setuptools.pypa.io/en/stable/userguide/pyproject_config.html
[project]
authors = [
{ name = "First Last", email = "[email protected]" },
]
authors = [{ name = "First Last", email = "[email protected]" }]
description = "A small example package"
name = "python_package"
# This means: Load the version from the package itself.
# See the section below: [tools.setuptools.dynamic]
dynamic = ["version", # version is loaded from the package
#"dependencies", # add if using requirements.txt
dynamic = [
"version", # version is loaded from the package
#"dependencies", # add if using requirements.txt
]
readme = "README.md"
requires-python = ">=3.9" # test all higher Python versions
Expand All @@ -17,7 +16,8 @@ classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
license = "MIT" # https://choosealicense.com/
# Also update LICENSE file if you pick another one
license = "GPL-3.0-or-later" # https://choosealicense.com/licenses/gpl-3.0/
# # add dependencies here: (use one of the two)
# dependencies = ["numpy", "pandas", "scipy", "matplotlib", "seaborn"]
# use requirements.txt instead of pyproject.toml for dependencies
Expand All @@ -44,20 +44,19 @@ docs = [
"sphinx-copybutton",
]
# local development options
dev = ["black[jupyter]", "ruff", "pytest"]
dev = ["black[jupyter]", "ruff", "pytest", "isort", "jupytext"]

# Configure the Ruff linter: Ignore error number 501
[tool.ruff]
# https://docs.astral.sh/ruff/rules/#flake8-bandit-s
# lint.ignore = ["E501"] # Ignore line length errors
# Allow lines to be as long as (default is 88 in black)

[tool.ruff.lint]
# https://docs.astral.sh/ruff/tutorial/#rule-selection
# 1. Enable flake8-bugbear (`B`) rules
# 2. Enable pycodestyle (`E`) errors and (`W`) warnings
# 3. Pyflakes (`F`) errors
extend-select = ["E", "W", "F", "B"]
# Ignore line length errors:
# ignore = ["E501"]

[build-system]
build-backend = "setuptools.build_meta"
Expand All @@ -69,3 +68,9 @@ requires = ["setuptools>=64", "setuptools_scm>=8"]

[tool.isort]
profile = "black"

# Script entry points, i.e. command line commands available after installing the package
# e.g. implemented using argparse
# Then you can type: `python-package-hello -h` in the terminal
[project.scripts]
python-package-hello = "python_package.cli:main"
4 changes: 2 additions & 2 deletions src/python_package/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

__version__ = metadata.version("python_package")

from .mockup import hello_world
from .mockup import hello_world, saved_world

# The __all__ variable is a list of variables which are imported
# when a user does "from example import *"
__all__ = ["hello_world"]
__all__ = ["hello_world", "saved_world"]
17 changes: 17 additions & 0 deletions src/python_package/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import argparse

from .mockup import hello_world


def main():
parser = argparse.ArgumentParser(description="Python Package CLI")
parser.add_argument(
"-n",
"--repeat",
type=int,
default=1,
help="Number of times to repeat the greeting hello world",
)
args = parser.parse_args()
msg = hello_world(args.repeat)
print(msg)
28 changes: 28 additions & 0 deletions src/python_package/mockup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,33 @@ def hello_world(n: int) -> str:
-------
str
str of 'hello world' n-times

Examples
--------
>>> hello_world(3)
'hello world hello world hello world'
"""
return " ".join(repeat("hello world", n))


def saved_world(filename: str) -> int:
"""
Count how many times 'hello world' is in a file.

Parameters
----------
filename : str
The file to read

Returns
-------
int
How many times 'hello world' is in the file

Examples
--------
>>> saved_world("not-real.txt") # doctest: +SKIP
"""
with open(filename, "r") as f:
content = f.read()
return content.count("hello world")
112 changes: 112 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
marp: true
theme: uncover
paginate: true
backgroundColor: #fff
---

# **Getting started with pytesting**

---

## Quick start

### Installation

Install pytest. e.g., from the "dev" dependencies

```bash
pip install ".[dev]"
```

### How to use

To execute the tests run e.g.

```bash
pytest
```

---

## Test development tips

---

### Folder and test naming

1. The tests for functions in `<filename>.py` should go in `tests/test_<filename>.py`

e.g., the tests for [python_package/mockup.py](../src/python_package/mockup.py) are in [tests/test_mockup.py](test_mockup.py)

2. The test names should start with `def test_<corresponding_function_name> ...`

e.g., `def test_hello_world(): ...`

---

### Some Pytest decorators

1. To indicate that the test function is expected to fail you can prepend

```python
@pytest.mark.xfail(raises=TypeError)
def test_hello_world_str(): ...
```

---

2. To setup and cleanup any resources for a test you can use [pytest fixtures with `yield`](https://dev.to/dawidbeno/understanding-yield-in-pytest-fixtures-4m38)

```python
@pytest.fixture
def temp_file():
# set up
< code to create a file>
# return
yield
the_file
# clean up
< code to remove the file>
```

---

### Doctests

You can also include tests in your docstrings using `>>>` followed by the expected result e.g.

```python
def hello_world(n):
"""
Prints 'hello world' n-times.
...

Examples
--------
>>> hello_world(3)
'hello world hello world hello world'
...
"""
```

Needs `addopts = --doctest-modules` in `pytest.ini` in root of directory

---

### Skipping in doctests

If you know that the test cannot succeed but would like to include an example usage in the docstring still then you can add `# doctest: +SKIP` e.g.

```python
def saved_world(filename):
"""
Count how many times 'hello world' is in a file.
...

Examples
--------
>>> saved_world("not-real.txt") # doctest: +SKIP
...
"""
```
Loading