From 69d6b4c5763a1492c81088e19e5f1ad1507e5209 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Tue, 16 Jul 2024 17:50:35 -0500 Subject: [PATCH 01/11] Add basic readme --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e9c76e8..d650cfd 100644 --- a/README.md +++ b/README.md @@ -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` for command-line arguments +* Assume the use of `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 + * yaml + * json + * HDF5 + * SQL + +### Modularity +* All runnable scripts should include a block like: + +``` python +if __name__ == __main__(): + do_main_task() +``` + +### Testing +* Always use `pytest` 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 From e33902f764ebf469cba910d383c18f437acc4859 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Tue, 16 Jul 2024 18:02:08 -0500 Subject: [PATCH 02/11] first template code --- do_task.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 do_task.py diff --git a/do_task.py b/do_task.py new file mode 100644 index 0000000..8ddaabb --- /dev/null +++ b/do_task.py @@ -0,0 +1,48 @@ +import argparse +import logging +import yaml + +logger = logging.getLogger(__name__) + +def perform_action(args, input_data): + + return "Some results based on args and input_data." + +def report_results(args, input_data, results): + + 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(): + + 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, + help="Level of verbosity for logging") + + return parser.parse_args() + +def read_input(input_filename): + + with open(input_filename, 'r') as yaml_file: + input_data = yaml.safe_load(yaml_file) + + return input_data + +def do_task(): + + args = task_args() + input_data = read_input(args.filename) + + results = perform_action(args, input_data) + report_results(results) + +if __name__ == "__main__": + do_task() \ No newline at end of file From b5d3ebc5b1a4cdb22da4c8a32cf8bce54a2a479c Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Tue, 16 Jul 2024 18:04:51 -0500 Subject: [PATCH 03/11] add sample test --- do_task.py | 2 +- test_do_task.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test_do_task.py diff --git a/do_task.py b/do_task.py index 8ddaabb..1a469d8 100644 --- a/do_task.py +++ b/do_task.py @@ -6,7 +6,7 @@ def perform_action(args, input_data): - return "Some results based on args and input_data." + return f"Some results based on args:\n{args}\n and input_data:\n{input_data}\n" def report_results(args, input_data, results): diff --git a/test_do_task.py b/test_do_task.py new file mode 100644 index 0000000..1af1734 --- /dev/null +++ b/test_do_task.py @@ -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 + + \ No newline at end of file From d7c8654884a1ccbcfea79bc957ffaf3c9bd10079 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Wed, 17 Jul 2024 07:37:10 -0500 Subject: [PATCH 04/11] add change to logging level based on args --- do_task.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/do_task.py b/do_task.py index 1a469d8..7ff65f4 100644 --- a/do_task.py +++ b/do_task.py @@ -24,7 +24,7 @@ def task_args(): parser.add_argument('filename', help="A filename to read for input") parser.add_argument("--verbose", "-v", - type=int, + type=int, default=0, help="Level of verbosity for logging") return parser.parse_args() @@ -39,6 +39,9 @@ def read_input(input_filename): def do_task(): args = task_args() + + logging.basicConfig(level=(logging.WARN-args.verbose)) + input_data = read_input(args.filename) results = perform_action(args, input_data) From d4d4190c2a97a2040125dccb1a8f49edcc52bcca Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Wed, 17 Jul 2024 09:43:41 -0500 Subject: [PATCH 05/11] PR comments incl PEP8 via Black --- do_task.py | 86 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/do_task.py b/do_task.py index 7ff65f4..e572987 100644 --- a/do_task.py +++ b/do_task.py @@ -4,48 +4,108 @@ logger = logging.getLogger(__name__) + 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" + ) - 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" + 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") + 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. - with open(input_filename, 'r') as yaml_file: + 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(): +def do_task(): + """Main function to perform the actions for this script.""" args = task_args() - - logging.basicConfig(level=(logging.WARN-args.verbose)) + + 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() \ No newline at end of file + do_task() From 53ca8b4dae57737bb26ce5aefa1eca982bfb110e Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Wed, 17 Jul 2024 09:43:57 -0500 Subject: [PATCH 06/11] add links per PR comments --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d650cfd..4531768 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ This repository serves as a template for new python projects and a way to expres ## Best Practices ### User input -* Always use `argparse` for command-line arguments -* Assume the use of `yaml` for structured input files unless there are compelling reasons for something else +* 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 - * yaml - * json - * HDF5 - * SQL + * [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: +* All runnable scripts [should include a block like](https://stackoverflow.com/questions/419163/what-does-if-name-main-do): ``` python if __name__ == __main__(): @@ -25,7 +25,7 @@ if __name__ == __main__(): ``` ### Testing -* Always use `pytest` for testing +* Always use [`pytest`](https://docs.pytest.org/en/8.2.x/) for testing * Introduce a simple continuous integration (CI) action ASAP ### Collaboration From 568db2fae268f3b84ad120a3a96443e3654376eb Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Thu, 18 Jul 2024 15:33:45 -0500 Subject: [PATCH 07/11] add some more style guidance to README --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4531768..3ee4380 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # py-template This repository serves as a template for new python projects and a way to express best practices -## Best Practices +## Best Practices - Coding Practices ### User input * Always use [`argparse`](https://docs.python.org/3/library/argparse.html) for command-line arguments @@ -34,3 +34,26 @@ if __name__ == __main__(): ### Packaging and installation * Introduce a `pyproject.toml` file ASAP + +## Best Practices - Style + +### Code formatting +* Follow PEP8 style guide, ideally with a tools like + [`black`](https://pypi.org/project/black/) to help enforce it, especially via + a plugin to your editor + +### Variable naming +* Generally, choose nouns for variables and verbs for methods +* Clear variable and method names can reduce the need for comments + +### Comments +* Include a docstring in every method +* Rely on clear variable and method names and add comments sparingly where the + intent/approach is non-intuitive + +### Modularity +* If you have cut & paste code in two different places, it probably should be a + method +* Even very short methods can be valuable if the method name makes the code more + readable +* Ideally, methods should be no longer than one screen worth of lines From bef58b9445a130d0cf5f0bb264f6976c1fa1c5eb Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Fri, 19 Jul 2024 11:02:46 -0500 Subject: [PATCH 08/11] PR merge rules --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3ee4380..de6e994 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ if __name__ == __main__(): ### Collaboration * Generate pull requests (PRs) with as little code change as possible * Include tests in all PRs +* Do not merge your own PR; there should always be at least one review by a + non-author, and a non-author should merge ### Packaging and installation * Introduce a `pyproject.toml` file ASAP From 716ebf9499709a617190d448cd111a429bae0a41 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Fri, 19 Jul 2024 11:09:51 -0500 Subject: [PATCH 09/11] add CI --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..edc70e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Sample CI + +on: + push: + workflow_dispatch: + +jobs: + main: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Apt dependencies + shell: bash + run: | + sudo apt -y update + sudo apt install -y python3-dev + + - name: Python dependencies + shell: bash + run: | + pip3 install numpy \ + pytest + + - name: Test + shell: bash + run: | + pytest -v . + \ No newline at end of file From 394bce17c9f6a1c275fb29b297a216bc106fe538 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Fri, 19 Jul 2024 11:11:33 -0500 Subject: [PATCH 10/11] make sure tests pass --- test_do_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_do_task.py b/test_do_task.py index 1af1734..74c0348 100644 --- a/test_do_task.py +++ b/test_do_task.py @@ -1,8 +1,8 @@ import do_task def test_perform_action(): - args = foo - input_data = bar + args = 'foo' + input_data = 'bar' exp = f"Some results based on args:\n{args}\n and input_data:\n{input_data}\n" From aba7ad8a3ad589496a84c08a4d7caa52b46f4f54 Mon Sep 17 00:00:00 2001 From: "Paul P.H. Wilson" Date: Fri, 19 Jul 2024 15:14:58 -0500 Subject: [PATCH 11/11] fix code error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de6e994..a1ac659 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This repository serves as a template for new python projects and a way to expres * All runnable scripts [should include a block like](https://stackoverflow.com/questions/419163/what-does-if-name-main-do): ``` python -if __name__ == __main__(): +if __name__ == "__main__": do_main_task() ```