Skip to content

Commit 27a502a

Browse files
committed
added base cli and log modules
1 parent 031f8b8 commit 27a502a

File tree

7 files changed

+150
-4
lines changed

7 files changed

+150
-4
lines changed

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def read(*names, **kwargs):
4646
DEV = [
4747
"pytest",
4848
"twine",
49+
# to test logging in the template
50+
"structlog",
4951
# optional dependency for test module
5052
"jupytext",
5153
"nbclient",

src/pkgmt/assets/template/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@
4141
"dev": DEV,
4242
},
4343
entry_points={
44-
# 'console_scripts': ['$project_name=$package_name.cli:cli'],
44+
"console_scripts": ["$project_name=$package_name.cli:cli"],
4545
},
4646
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
Sample CLI (requires click, tested with click==8.1.3)
3+
"""
4+
5+
import click
6+
7+
8+
@click.group()
9+
def cli():
10+
"""Command-line interface"""
11+
pass
12+
13+
14+
@cli.command()
15+
@click.argument("name")
16+
def hello(name):
17+
"""Say hello to someone"""
18+
print(f"Hello, {name}!")
19+
20+
21+
@cli.command()
22+
@click.argument("name")
23+
def log(name):
24+
"""Log a message"""
25+
from $package_name.log import configure_file_and_print_logger, get_logger
26+
27+
configure_file_and_print_logger()
28+
logger = get_logger()
29+
logger.info(f"Hello, {name}!", name=name)
30+
31+
32+
if __name__ == "__main__":
33+
cli()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
A sample logger (requires structlog, tested with structlog==25.1.0)
3+
4+
Usage
5+
-----
6+
7+
>>> from $package_name.log import configure_file_and_print_logger, get_logger
8+
>>> configure_file_and_print_logger("app.log")
9+
>>> # OR
10+
>>> configure_print_logger()
11+
>>> logger = get_logger()
12+
>>> logger.info("Hello, world!")
13+
"""
14+
15+
import logging
16+
import structlog
17+
from typing import Any, TextIO
18+
import os
19+
20+
import structlog
21+
from structlog import WriteLogger, PrintLogger
22+
23+
24+
class CustomLogger:
25+
"""
26+
A custom logger that writes to a file and prints to the console
27+
"""
28+
29+
def __init__(self, file: TextIO | None = None):
30+
self._file = file
31+
self._write_logger = WriteLogger(self._file)
32+
self._print_logger = PrintLogger()
33+
34+
def msg(self, message: str) -> None:
35+
self._write_logger.msg(message)
36+
self._print_logger.msg(message)
37+
38+
log = debug = info = warn = warning = msg
39+
fatal = failure = err = error = critical = exception = msg
40+
41+
42+
class CustomLoggerFactory:
43+
def __init__(self, file: TextIO | None = None):
44+
self._file = file
45+
46+
def __call__(self, *args: Any) -> CustomLogger:
47+
return CustomLogger(self._file)
48+
49+
50+
def configure_file_and_print_logger(file_path: str = "app.log") -> None:
51+
structlog.configure(
52+
processors=[
53+
structlog.contextvars.merge_contextvars,
54+
structlog.processors.add_log_level,
55+
structlog.processors.StackInfoRenderer(),
56+
structlog.dev.set_exc_info,
57+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
58+
structlog.processors.JSONRenderer(),
59+
],
60+
wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET),
61+
context_class=dict,
62+
logger_factory=CustomLoggerFactory(open(file_path, "at")),
63+
cache_logger_on_first_use=False,
64+
)
65+
66+
67+
def configure_print_logger() -> None:
68+
structlog.configure(
69+
processors=[
70+
structlog.contextvars.merge_contextvars,
71+
structlog.processors.add_log_level,
72+
structlog.processors.StackInfoRenderer(),
73+
structlog.dev.set_exc_info,
74+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
75+
structlog.dev.ConsoleRenderer(),
76+
],
77+
wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET),
78+
context_class=dict,
79+
logger_factory=structlog.PrintLoggerFactory(),
80+
cache_logger_on_first_use=False,
81+
)
82+
83+
84+
def configure_no_logging() -> None:
85+
structlog.configure(
86+
logger_factory=structlog.PrintLoggerFactory(open(os.devnull, "w")),
87+
)
88+
89+
90+
def get_logger():
91+
return structlog.get_logger()

src/pkgmt/assets/template/tasks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ def setup(c, version=None):
66
"""
77
Setup dev environment, requires conda
88
"""
9-
version = version or "3.9"
10-
suffix = "" if version == "3.9" else version.replace(".", "")
9+
version = version or "3.12"
10+
suffix = "" if version == "3.12" else version.replace(".", "")
1111
env_name = f"$project_name{suffix}"
1212

1313
c.run(f"conda create --name {env_name} python={version} --yes")

src/pkgmt/new.py

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def package(name, use_setup_py=False):
3434
"pyproject.toml",
3535
"pyproject-setup.toml",
3636
".github/workflows/ci.yml",
37+
"src/package_name/cli.py",
38+
"src/package_name/log.py",
3739
):
3840
render_inplace(
3941
root / file,

tests/test_new.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
from unittest.mock import ANY
13
from pathlib import Path
24
import subprocess
35
from pkgmt import new
@@ -8,7 +10,7 @@
810
@pytest.fixture
911
def uninstall():
1012
yield
11-
subprocess.check_call(["pip", "uninstall", "somepkg", "-y"])
13+
subprocess.check_call(["pip", "uninstall", "some-cool-pkg", "-y"])
1214

1315

1416
def test_package_setup_py(tmp_empty, uninstall):
@@ -31,6 +33,14 @@ def test_package_setup_py(tmp_empty, uninstall):
3133
assert 'python -c "import some_cool_pkg"' in ci
3234
assert "graft src/some_cool_pkg/assets" in manifest
3335

36+
subprocess.check_call(["some-cool-pkg", "log", "user"])
37+
assert json.loads(Path("app.log").read_text()) == {
38+
"name": "user",
39+
"event": "Hello, user!",
40+
"level": "info",
41+
"timestamp": ANY,
42+
}
43+
3444

3545
def test_package_pyproject_toml(tmp_empty, uninstall):
3646
new.package("some-cool_pkg", use_setup_py=False)
@@ -56,3 +66,11 @@ def test_package_pyproject_toml(tmp_empty, uninstall):
5666
assert 'package-data = { "some_cool_pkg" = ["assets/*", "*.md"] }' in pyproject
5767

5868
assert 'python -c "import some_cool_pkg"' in ci
69+
70+
subprocess.check_call(["some-cool-pkg", "log", "user"])
71+
assert json.loads(Path("app.log").read_text()) == {
72+
"name": "user",
73+
"event": "Hello, user!",
74+
"level": "info",
75+
"timestamp": ANY,
76+
}

0 commit comments

Comments
 (0)