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

Release cycle, ? 2025 #449

Draft
wants to merge 46 commits into
base: main
Choose a base branch
from
Draft
Changes from 6 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
53aa24f
Remove unused method from `Parameters`
TimothyWillard Dec 5, 2024
098e13d
Line length formatting
TimothyWillard Dec 5, 2024
8c8df8b
New documentation and installation script
emprzy Dec 9, 2024
0ebbf08
Update local-installation.md
emprzy Dec 9, 2024
1eee527
Minor styling edits to `seeding.py`
TimothyWillard Dec 9, 2024
2d68856
Remove `modinf` arg from `_DataFrame2NumbaDict`
TimothyWillard Dec 9, 2024
a12150c
Add `ModelInfo.get_seeding_data`
TimothyWillard Dec 10, 2024
8ba5f0c
Update other references to `Seeding.get_from_*`
TimothyWillard Dec 10, 2024
e31cac8
Add documentation to the `Seeding` class
TimothyWillard Dec 10, 2024
692865f
Document `ModelInfo.get_seeding_data`
TimothyWillard Dec 10, 2024
0b9c351
Update `local_install_or_update` and `local-installation.md`
emprzy Dec 16, 2024
d812220
Removing file extension from `local_install_or_update` file
emprzy Dec 18, 2024
67ccbff
Update local_install_or_update
emprzy Dec 19, 2024
cbecd91
Incorporate `conda` env setup into installation script and update doc…
emprzy Dec 20, 2024
8db7923
Update local_install_or_update
emprzy Dec 26, 2024
7b97ac7
Add `Jinja2` dep to `gempyor`
TimothyWillard Jan 6, 2025
5abf319
Set `include-package-data` to `true`
TimothyWillard Nov 26, 2024
0d6f517
Include templates with `include-data` directive
TimothyWillard Dec 2, 2024
ca17746
Jinja2 templating infrastructure
TimothyWillard Oct 29, 2024
a997367
Fix Jinja2 loader
TimothyWillard Nov 26, 2024
20bb555
Provide only pkg name to Jinja2 `PackageLoader`
TimothyWillard Nov 26, 2024
1d82cd4
Prefer `FileSystemLoader` over `PackageLoader`
TimothyWillard Nov 26, 2024
71e2f47
Logging infrastructure for `gempyor`
TimothyWillard Oct 29, 2024
e090873
Only propagate logger in pytest
TimothyWillard Oct 30, 2024
a983af2
`black` linting for `gempyor.logging`
TimothyWillard Jan 7, 2025
cd3392e
Merge pull request #418 from emprzy/installation_documentation
emprzy Jan 7, 2025
39be84e
Merge dev into feature/310/logging-infra
TimothyWillard Jan 7, 2025
cd2bfd0
Add `log_cli_inputs` utility
TimothyWillard Nov 5, 2024
a333f6b
Add custom duration click param type
TimothyWillard Jan 7, 2025
f2edc27
Add custom click param type for memory
TimothyWillard Jan 7, 2025
33ad611
Document info/shared_cli, MemoryParamType int
TimothyWillard Jan 7, 2025
bbfa1c2
Assume unitless numbers are in default unit
TimothyWillard Nov 25, 2024
39bfbdc
Remove unused `_jinja` helpers
TimothyWillard Jan 8, 2025
085beca
Call positional args with names
TimothyWillard Jan 8, 2025
857f9e6
Explicit unit control for custom click types
TimothyWillard Jan 8, 2025
dcb899b
Create internal `gempyor._click` module
TimothyWillard Jan 8, 2025
7426548
Merge pull request #447 from HopkinsIDD/feature/310/logging-infra
TimothyWillard Jan 10, 2025
da7d816
Merge pull request #414 from HopkinsIDD/feature/276/remove-dead-methods
TimothyWillard Jan 10, 2025
4071d37
Merge dev into feature/365/convenience-click-types
TimothyWillard Jan 10, 2025
5eee4a6
Merge pull request #446 from HopkinsIDD/feature/365/templating-infra
TimothyWillard Jan 13, 2025
533fcd9
Merge pull request #448 from HopkinsIDD/feature/365/convenience-click…
TimothyWillard Jan 13, 2025
4917d62
Merge main into dev
TimothyWillard Jan 15, 2025
5e6e622
Merge pull request #422 from HopkinsIDD/feature/397/remove-seeding-de…
TimothyWillard Jan 15, 2025
320188f
Merge main into dev
TimothyWillard Jan 17, 2025
8e6e51c
Merge main into dev
TimothyWillard Jan 23, 2025
986423a
Merge main into dev
TimothyWillard Jan 23, 2025
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
169 changes: 169 additions & 0 deletions flepimop/gempyor_pkg/src/gempyor/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
Logging utilities for consistent script output.

This module provides functionality for creating consistent outputs from CLI tools
provided by this package. Currently exported are:
- `ClickHandler`: Custom logging handler specifically designed for CLI output using
click.
- `get_script_logger`: Factory for creating a logger instance with a consistent style
across CLI tools.
"""

__all__ = ["ClickHandler", "get_script_logger"]


import logging
import os
import sys
from typing import Any, IO

import click


DEFAULT_LOG_FORMAT = "%(asctime)s:%(levelname)s:%(name)s> %(message)s"


class ClickHandler(logging.Handler):
"""
Custom logging handler specifically for click based CLI tools.
"""

_punctuation = (".", ",", "?", "!", ":")

def __init__(
self,
level: int | str = 0,
file: IO[Any] | None = None,
nl: bool = True,
err: bool = False,
color: bool | None = None,
punctuate: bool = True,
) -> None:
"""
Initialize an instance of the click handler.

Args:
level: The logging level to use for this handler.
file: The file to write to. Defaults to stdout.
nl: Print a newline after the message. Enabled by default.
err: Write to stderr instead of stdout.
color: Force showing or hiding colors and other styles. By default click
will remove color if the output does not look like an interactive
terminal.
punctuate: A boolean indicating if punctuation should be added to the end
of a log message provided if missing.

Notes:
For more details on the `file`, `nl`, `err`, and `color` args please refer
to [`click.echo`](https://click.palletsprojects.com/en/8.1.x/api/#click.echo).
"""
super().__init__(level)
self._file = file
self._nl = nl
self._err = err
self._color = color
self._punctuate = punctuate

def emit(self, record: logging.LogRecord) -> None:
"""
Emit a given log record via `click.echo`

Args:
record: The log record to output.

See Also:
[`logging.Handler.emit`](https://docs.python.org/3/library/logging.html#logging.Handler.emit)
"""
msg = self.format(record)
msg = f"{msg}." if self._punctuate and not msg.endswith(self._punctuation) else msg
click.echo(
message=msg, file=self._file, nl=self._nl, err=self._err, color=self._color
)


def get_script_logger(
name: str,
verbosity: int,
handler: logging.Handler | None = None,
log_format: str = DEFAULT_LOG_FORMAT,
) -> logging.Logger:
"""
Create a logger for use in scripts.

Args:
name: The name to display in the log message, useful for locating the source
of logging messages. Almost always `__name__`.
verbosity: A non-negative integer for the verbosity level.
handler: An optional logging handler to use in creating the logger returned, or
`None` to just use the `ClickHandler`.
log_format: The format to use for logged messages. Passed directly to the `fmt`
argument of [logging.Formatter](https://docs.python.org/3/library/logging.html#logging.Formatter).

Returns:
An instance of `logging.Logger` that has the appropriate level set based on
`verbosity` and a custom handler for outputting for CLI tools.

Examples:
>>> from gempyor.logging import get_script_logger
>>> logger = get_script_logger(__name__, 3)
>>> logger.info("This is a log info message")
2024-10-29 16:07:20,272:INFO:__main__> This is a log info message.
"""
logger = logging.getLogger(name)
logger.setLevel(_get_logging_level(verbosity))
handler = ClickHandler() if handler is None else handler
log_formatter = logging.Formatter(log_format)
for old_handler in logger.handlers:
logger.removeHandler(old_handler)
handler.setFormatter(log_formatter)
logger.addHandler(handler)
# pytest-dev/pytest#3697
logger.propagate = os.path.basename(sys.argv[0]) == "pytest" if sys.argv else False
return logger


def _get_logging_level(verbosity: int) -> int:
"""
An internal method to convert verbosity to a logging level.

Args:
verbosity: A non-negative integer for the verbosity level or level from
`logging` that will be returned as is.

Examples:
>>> _get_logging_level(0)
40
>>> _get_logging_level(1)
30
>>> _get_logging_level(2)
20
>>> _get_logging_level(3)
10
>>> _get_logging_level(4)
10
>>> import logging
>>> _get_logging_level(logging.ERROR) == logging.ERROR
True

Raises:
ValueError: If `verbosity` is less than zero.

Returns:
The log level from the `logging` module corresponding to the given `verbosity`.
"""
if verbosity < 0:
raise ValueError(f"`verbosity` must be non-negative, was given '{verbosity}'.")
if verbosity in (
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.CRITICAL,
):
return verbosity
verbosity_to_logging_level = {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
}
return verbosity_to_logging_level.get(verbosity, logging.DEBUG)
49 changes: 49 additions & 0 deletions flepimop/gempyor_pkg/src/gempyor/shared_cli.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
import click
import confuse

from .logging import get_script_logger
from .utils import config, as_list

__all__ = []
@@ -276,3 +277,51 @@ def _parse_option(param: click.Parameter, value: Any) -> Any:
cfg[option] = _parse_option(config_file_options[option], value)

return cfg


def log_cli_inputs(kwargs: dict[str, Any], verbosity: int | None = None) -> None:
"""
Log CLI inputs for user debugging.

This function only logs debug messages so the verbosity has to be set quite high
to see the output of this function.

Args:
kwargs: The CLI arguments given as a dictionary of key word arguments.
verbosity: The verbosity level of the CLI tool being used or `None` to infer
from the given `kwargs`.

Examples:
>>> from gempyor.shared_cli import log_cli_inputs
>>> log_cli_inputs({"abc": 123, "def": True}, 3)
2024-11-05 09:27:58,884:DEBUG:gempyor.shared_cli> CLI was given 2 arguments:
2024-11-05 09:27:58,885:DEBUG:gempyor.shared_cli> abc = 123.
2024-11-05 09:27:58,885:DEBUG:gempyor.shared_cli> def = True.
>>> log_cli_inputs({"abc": 123, "def": True}, 2)
>>> from pathlib import Path
>>> kwargs = {
... "input_file": Path("config.in"),
... "stochastic": True,
... "cluster": "longleaf",
... "verbosity": 3,
... }
>>> log_cli_inputs(kwargs)
2024-11-05 09:29:21,666:DEBUG:gempyor.shared_cli> CLI was given 4 arguments:
2024-11-05 09:29:21,667:DEBUG:gempyor.shared_cli> input_file = /Users/twillard/Desktop/GitHub/HopkinsIDD/flepiMoP/flepimop/gempyor_pkg/config.in.
2024-11-05 09:29:21,667:DEBUG:gempyor.shared_cli> stochastic = True.
2024-11-05 09:29:21,668:DEBUG:gempyor.shared_cli> cluster = longleaf.
2024-11-05 09:29:21,668:DEBUG:gempyor.shared_cli> verbosity = 3.
"""
verbosity = kwargs.get("verbosity") if verbosity is None else verbosity
if verbosity is None:
return
logger = get_script_logger(__name__, verbosity)
longest_key = -1
total_keys = 0
for k, _ in kwargs.items():
longest_key = len(k) if len(k) > longest_key else longest_key
total_keys += 1
logger.debug("CLI was given %u arguments:", total_keys)
for k, v in kwargs.items():
v = v.absolute() if isinstance(v, pathlib.Path) else v
logger.debug("%s = %s", k.ljust(longest_key, " "), v)
32 changes: 32 additions & 0 deletions flepimop/gempyor_pkg/tests/logging/test__get_logging_level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

import pytest

from gempyor.logging import _get_logging_level


@pytest.mark.parametrize("verbosity", (-1, -100))
def test__get_logging_level_negative_verbosity_value_error(verbosity: int) -> None:
with pytest.raises(
ValueError, match=f"`verbosity` must be non-negative, was given '{verbosity}'."
):
_get_logging_level(verbosity)


@pytest.mark.parametrize(
("verbosity", "expected_level"),
(
(logging.ERROR, logging.ERROR),
(logging.WARNING, logging.WARNING),
(logging.INFO, logging.INFO),
(logging.DEBUG, logging.DEBUG),
(0, logging.ERROR),
(1, logging.WARNING),
(2, logging.INFO),
(3, logging.DEBUG),
(4, logging.DEBUG),
(5, logging.DEBUG),
),
)
def test__get_logging_level_output_validation(verbosity: int, expected_level: int) -> None:
assert _get_logging_level(verbosity) == expected_level
71 changes: 71 additions & 0 deletions flepimop/gempyor_pkg/tests/logging/test_click_handler_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import io
import logging
from typing import Any, IO

import pytest

from gempyor.logging import ClickHandler


@pytest.mark.parametrize(
"level",
(logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL),
)
@pytest.mark.parametrize("file", (None, io.StringIO))
@pytest.mark.parametrize("nl", (True, False))
@pytest.mark.parametrize("err", (False, True))
@pytest.mark.parametrize("color", (None, False))
@pytest.mark.parametrize("punctuate", (True, False))
def test_click_handler_init(
level: int | str,
file: IO[Any] | None,
nl: bool,
err: bool,
color: bool | None,
punctuate: bool,
) -> None:
handler = ClickHandler(
level=level, file=file, nl=nl, err=err, color=color, punctuate=punctuate
)
assert handler.level == level
assert handler._file == file
assert handler._nl == nl
assert handler._err == err
assert handler._color == color
assert handler._punctuate == punctuate


@pytest.mark.parametrize("punctuate", (True, False))
@pytest.mark.parametrize(
"msg",
(
"This is a message",
"Another message.",
"Start to a list:",
"Middle of a sentence,",
"Oh-no!",
"Question?",
),
)
def test_click_handler_punctuation_formatting(punctuate: bool, msg: str) -> None:
buffer = io.StringIO()
handler = ClickHandler(level=logging.DEBUG, file=buffer, punctuate=punctuate)
log_record = logging.LogRecord(
name="",
level=logging.DEBUG,
pathname="",
lineno=1,
msg=msg,
args=None,
exc_info=None,
)
handler.emit(log_record)
buffer.seek(0)
handler_msg = buffer.getvalue()
if not punctuate:
assert handler_msg == msg + "\n"
else:
assert (
handler_msg
== (msg if msg.endswith(ClickHandler._punctuation) else f"{msg}.") + "\n"
)
Loading