Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Starting a repo with basic python code best practices #1

Merged
merged 11 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,36 @@
# py-template
Template for a new python project
This repository serves as a template for new python projects and a way to express best practices

## Best Practices

### User input
* Always use [`argparse`](https://docs.python.org/3/library/argparse.html) for command-line arguments
* Assume the use of [`yaml`](https://python.land/data-processing/python-yaml) for structured input files unless there are compelling reasons for something else

### User output
* Always use `logger` for status output
* Carefully choose an output format for standard formats, considering the following in order of priority:
* [CSV](https://docs.python.org/3/library/csv.html) with direct support in NumPy, Pandas, etc
* [yaml](https://python.land/data-processing/python-yaml)
* [json](https://docs.python.org/3/library/json.html)
* [HDF5](https://www.h5py.org/)
* SQL, for example [SQLite](https://docs.python.org/3/library/sqlite3.html)

### Modularity
* All runnable scripts [should include a block like](https://stackoverflow.com/questions/419163/what-does-if-name-main-do):

``` python
if __name__ == __main__():
do_main_task()
```

### Testing
* Always use [`pytest`](https://docs.pytest.org/en/8.2.x/) for testing
* Introduce a simple continuous integration (CI) action ASAP

### Collaboration
* Generate pull requests (PRs) with as little code change as possible
* Include tests in all PRs

### Packaging and installation
* Introduce a `pyproject.toml` file ASAP
111 changes: 111 additions & 0 deletions do_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import argparse
import logging
import yaml

logger = logging.getLogger(__name__)
gonuke marked this conversation as resolved.
Show resolved Hide resolved


def perform_action(args, input_data):
"""Perform some action

Following the principle of separation of concerns, this method only
performs the action, but does not have any input or output.

inputs
------
args : argparse object with many possible members based on argparse configuration
input_data : dictionary of input data read from YAML file

outputs
--------
string that describes the input quantities

"""

return f"Some results based on args:\n{args}\n and input_data:\n{input_data}\n"


def report_results(args, input_data, results):
"""Report the result of the action

Following the principle of separation of concerns, this method only
performs output and does not do any actions.

inputs
------
args : argparse object with many possible members based on argparse configuration
input_data : dictionary of input data read from YAML file
results : the results of the action

outputs
--------
None

"""

logger.info(
f"Based on the values of args:\n{args}\n and input_data:\n{input_data}\n"
+ "Here are the results:\n{results}\n"
)


def task_args():
"""Setup argparse for this script

Add an argparse.ArgumentParser with standard entries of:
`filename` : required positional argument
`verbose` : optional keyword argument

inputs
-------
None

outputs
--------
argparse object with various members depending on configuration
"""

parser = argparse.ArgumentParser(
prog="do-task",
description="A program to perform a certain task.",
epilog="Some text at the bottom of the help message",
)

parser.add_argument("filename", help="A filename to read for input")
parser.add_argument(
"--verbose", "-v", type=int, default=0, help="Level of verbosity for logging"
)

return parser.parse_args()


def read_input(input_filename):
"""Read input from yaml

Read input in YAML format from a specified filename.

inputs
-------
input_filename : a string with a filename/path accessible from the current location
"""

with open(input_filename, "r") as yaml_file:
input_data = yaml.safe_load(yaml_file)

return input_data


def do_task():
"""Main function to perform the actions for this script."""
args = task_args()

logging.basicConfig(level=(logging.WARN - args.verbose))

input_data = read_input(args.filename)

results = perform_action(args, input_data)
report_results(results)


if __name__ == "__main__":
do_task()
13 changes: 13 additions & 0 deletions test_do_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import do_task

def test_perform_action():
args = foo
input_data = bar

exp = f"Some results based on args:\n{args}\n and input_data:\n{input_data}\n"

obs = do_task.perform_action(args, input_data)

assert exp == obs