Skip to content

Commit 4b07bab

Browse files
committed
interim commit
1 parent 100c47c commit 4b07bab

File tree

11 files changed

+478
-20
lines changed

11 files changed

+478
-20
lines changed

.github/workflows/main.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ jobs:
2727
run: make qa
2828

2929
- name: Upload coverage reports to Codecov
30-
uses: codecov/codecov-action@v3
30+
uses: codecov/codecov-action@v5
31+
if: ${{ !cancelled() }}
32+
files: ./.coverage.xml
3133
env:
3234
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
35+
36+
- name: Upload test results to Codecov
37+
if: ${{ !cancelled() }}
38+
uses: codecov/test-results-action@v1
39+
files: ./.junit.xml
40+
with:
41+
token: ${{ secrets.CODECOV_TOKEN }}

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
coverage.json
1+
.junit.xml
22

33
# Byte-compiled / optimized / DLL files
44
__pycache__/
@@ -15,7 +15,6 @@ param_dict.json
1515
*java*
1616
application.sh
1717
lgcy*
18-
*demo*
1918
~*
2019
# Distribution / packaging
2120
.Python

.readthedocs.yml

Lines changed: 0 additions & 15 deletions
This file was deleted.

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ clean:
4040
@rm -rf .mypy_cache
4141
@rm -rf .pytest_cache
4242
@rm -rf .ruff_cache
43-
#
43+
@rm .coverage*
44+
@rm .junit.xml
45+
4446
# Recipe stolen from: https://gist.github.com/prwhite/8168133?permalink_comment_id=4160123#gistcomment-4160123
4547
.PHONY: help
4648
help: ## Show help message

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ minversion = "7.4.0"
5050
addopts = [
5151
"--cov=src/buzz",
5252
"--cov-report=term-missing",
53-
"--cov-report=json",
5453
"--cov-fail-under=90",
54+
"--cov-report=xml:.coverage.xml",
55+
"--junitxml=.junit.xml",
56+
"--override-ini=junit_family=legacy",
5557
]
5658

5759
[tool.ruff]

src/demo/__init__.py

Whitespace-only changes.

src/demo/enforce_defined.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from typing_extensions import override
5+
6+
from buzz import enforce_defined
7+
8+
9+
def simple_enforce_defined():
10+
"""
11+
This function demonstrates the simplest use of the enforce_defined
12+
function. This function can be used any time a a value needs to be checked
13+
to ensure it is defined. This function is best used in an assignment
14+
expression so that static type checkers won't complain if you attempt to
15+
access an attribute of a value that may not be undefined.
16+
"""
17+
val: str | None = "test-value"
18+
val = enforce_defined(val)
19+
# I can safely access the `upper()` method of `val` now."
20+
# There are also no type errors because `val` is now guaranteed to be a string.
21+
val.upper()
22+
23+
enforce_defined(None)
24+
25+
26+
def complex_enforce_defined():
27+
"""
28+
This function demonstrates a more complex usage of the enforce_defined
29+
function. It shows the ability to supply a custom message, raise specific
30+
exception types, and pass args and kwargs to the raised exception.
31+
"""
32+
class DemoException(Exception):
33+
def __init__(self, message: str, demo_arg: Any, demo_kwarg: Any | None = None):
34+
super().__init__(message)
35+
self.demo_arg: Any = demo_arg
36+
self.demo_kwarg: Any = demo_kwarg
37+
38+
@override
39+
def __str__(self):
40+
return f"{super().__str__()} (with demo_arg={self.demo_arg} and demo_kwarg={self.demo_kwarg})"
41+
42+
def get_val(defined: bool = True) -> str | None:
43+
if defined:
44+
return "test-value"
45+
else:
46+
return None
47+
48+
val = enforce_defined(get_val(defined=True), "This condition should pass")
49+
val.upper()
50+
val = enforce_defined(
51+
get_val(defined=False),
52+
f'Value is not defined!!!',
53+
raise_exc_class=DemoException,
54+
raise_args=["jawa"],
55+
raise_kwargs=dict(demo_kwarg="ewok"),
56+
)
57+
val.upper()

src/demo/handle_errors.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""
2+
This example set demonstrates the use of the handle_errors context manager.
3+
The handle_errors context manager effectively wraps a block of code with
4+
a try-except without having to explicitly declare them. Additionally, it wraps
5+
all caught exceptions from the block in custom error messages
6+
"""
7+
from __future__ import annotations
8+
9+
from typing import Any
10+
11+
from buzz import handle_errors, DoExceptParams
12+
13+
14+
def simple_handle_errors():
15+
"""
16+
This function demonstrates the simplest use of the handle_errors ctx_mgr.
17+
Note how the nested exception type is ValueError. The error produced by
18+
the handle_errors context manager should be of type HandleError. The
19+
resulting exception message will include the message supplied to the
20+
handle_error context manager as well as the message of the nested exception
21+
"""
22+
with handle_errors("something went wrong (simple example)"):
23+
print("here we are fine")
24+
raise ValueError("here we die")
25+
print("we should not get here") # pyright: ignore[reportUnreachable]
26+
27+
28+
def absorbing_handle_errors():
29+
"""
30+
This function demonstrates a more complex usage of the handle_errors
31+
context manager where exceptions are absorbed. The following features
32+
are demonstrated:
33+
34+
* Handling a specific exception type with `exception_class`
35+
* Absorbing exceptions by setting `raise_exc_class` to `None`
36+
* Branching with `do_except`, `do_else`, and `do_finally`
37+
"""
38+
def _handler_function(dep: DoExceptParams):
39+
"""
40+
This function is a helper function for handling an exception from
41+
the handle_errors ctx_mgr.
42+
"""
43+
print(f"do_except() was called!")
44+
print(f"Handling exception: {dep.err}")
45+
print(f"Final Message: {dep.final_message}")
46+
47+
with handle_errors(
48+
f"something went wrong (complex example)",
49+
raise_exc_class=None,
50+
do_except=_handler_function,
51+
do_else=lambda: print('do_else() was called!'),
52+
do_finally=lambda: print('do_finally() was called!'),
53+
):
54+
print("here we are fine")
55+
raise ValueError("here we die")
56+
print("we should not get here") # pyright: ignore[reportUnreachable]
57+
58+
59+
def multiple_handle_errors():
60+
"""
61+
This function demonstrates handling more than one exception type with the
62+
handle_errors context manager. The following features are demonstrated:
63+
64+
* Handling multiple exception types
65+
66+
Note that the final `TypeError` is _not_ handled!
67+
"""
68+
def _handler_function(dep: DoExceptParams):
69+
"""
70+
This function is a helper function for handling an exception from
71+
the handle_errors context manager.
72+
"""
73+
print(f"Handling exception type {dep.err.__class__}: {dep.final_message}")
74+
75+
for exception_class in (ValueError, RuntimeError, AttributeError, TypeError):
76+
with handle_errors(
77+
f"something went wrong (multiple example)",
78+
handle_exc_class=(ValueError, RuntimeError, AttributeError),
79+
do_except=_handler_function,
80+
raise_exc_class=None,
81+
):
82+
print("here we are fine")
83+
raise exception_class("here we die")
84+
print("we should not get here") # pyright: ignore[reportUnreachable]
85+
86+
87+
def specific_handle_errors():
88+
"""
89+
This function demonstrates how handle_errors can be used to raise a specific
90+
exception type that wraps the error message of the handled exception. The
91+
following features are demonstrated:
92+
93+
* Raising a specific exception instance using the `raise_exc_class` parameter
94+
* Passing along ``raise_args` and `raise_kwargs` when raising the exception
95+
"""
96+
class DerivedError(Exception):
97+
98+
def __init__(self, message: str, init_arg: Any, init_kwarg: Any | None = None):
99+
super().__init__(message)
100+
print(f"Derived Error initialized with: {init_arg=}, {init_kwarg=}")
101+
102+
with handle_errors(
103+
f"something went wrong (specific example)",
104+
raise_exc_class=DerivedError,
105+
raise_args=["init arg"],
106+
raise_kwargs=dict(init_kwarg="init kwarg"),
107+
):
108+
print("here we are fine")
109+
raise ValueError("here we die")
110+
print("we should not get here") # pyright: ignore[reportUnreachable]

src/demo/helpers.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from __future__ import annotations
2+
3+
import io
4+
import inspect
5+
import sys
6+
import textwrap
7+
from typing import Any
8+
from collections.abc import Callable
9+
from dataclasses import dataclass
10+
11+
from rich import box
12+
from rich.text import Text
13+
14+
import snick
15+
from rich.console import Console, Group, RenderableType
16+
from rich.panel import Panel
17+
from rich.prompt import Confirm
18+
from rich.markdown import Markdown
19+
from rich.rule import Rule
20+
21+
22+
@dataclass
23+
class Decomposed:
24+
name: str
25+
docstring: str
26+
source: str
27+
28+
29+
@dataclass
30+
class Captured:
31+
error: Exception | None = None
32+
output: str | None = None
33+
34+
BlankLine = Rule(characters=" ")
35+
36+
def decompose(func: Callable[..., Any]) -> Decomposed:
37+
"""
38+
This is really hacky.
39+
40+
Maybe improve this sometime.
41+
"""
42+
name = func.__name__
43+
44+
if func.__doc__ is None:
45+
raise RuntimeError("Can't demo a function with no docstring!")
46+
docstring = textwrap.dedent(func.__doc__).strip()
47+
48+
source_lines = inspect.getsourcelines(func)[0]
49+
first_quotes = False
50+
code_start_index = 0
51+
for i, line in enumerate(source_lines):
52+
if line.strip() == '"""':
53+
if not first_quotes:
54+
first_quotes = True
55+
else:
56+
code_start_index = i
57+
break
58+
if code_start_index == 0:
59+
raise RuntimeError("Failed to strip function declaration and docstring!")
60+
source = textwrap.dedent("".join(source_lines[code_start_index+1:]))
61+
62+
return Decomposed(name=name, docstring=docstring, source=source)
63+
64+
65+
def capture(demo: Callable[..., None]) -> Captured:
66+
cap = Captured()
67+
68+
string_buffer = io.StringIO()
69+
original_stdout = sys.stdout
70+
sys.stdout = string_buffer
71+
72+
try:
73+
demo()
74+
except Exception as exc:
75+
cap.error = exc
76+
finally:
77+
sys.stdout = original_stdout
78+
79+
dump = string_buffer.getvalue()
80+
if dump:
81+
cap.output = dump
82+
83+
return cap
84+
85+
86+
def run_demo(demo: Callable[..., None], console: Console) -> bool:
87+
console.clear()
88+
89+
decomposed = decompose(demo)
90+
cap: Captured = capture(demo)
91+
92+
parts: list[RenderableType] = [
93+
Markdown(decomposed.docstring),
94+
BlankLine,
95+
BlankLine,
96+
Panel(
97+
Markdown(
98+
"\n".join([
99+
"```python",
100+
decomposed.source,
101+
"```",
102+
])
103+
),
104+
title=f"Here is the source code for [green]{decomposed.name}()[/green]",
105+
title_align="left",
106+
padding=1,
107+
expand=False,
108+
box=box.SIMPLE,
109+
),
110+
]
111+
112+
if cap.output:
113+
parts.extend([
114+
BlankLine,
115+
BlankLine,
116+
Panel(
117+
Markdown(
118+
snick.conjoin(
119+
"```text",
120+
cap.output,
121+
"```"
122+
),
123+
),
124+
title=f"Here is the output captured from [green]{decomposed.name}()[/green]",
125+
title_align="left",
126+
padding=1,
127+
expand=False,
128+
box=box.SIMPLE,
129+
),
130+
])
131+
132+
if cap.error:
133+
parts.extend([
134+
BlankLine,
135+
BlankLine,
136+
Panel(
137+
f"[red]{cap.error.__class__.__name__}[/red]: [yellow]{str(cap.error)}[/yellow]",
138+
title=f"Here is the uncaught exception from [green]{decomposed.name}()[/green]",
139+
title_align="left",
140+
padding=1,
141+
expand=False,
142+
box=box.SIMPLE,
143+
)
144+
])
145+
146+
console.print(
147+
Panel(
148+
Group(*parts),
149+
padding=1,
150+
title=f"[yellow]{decomposed.name}() demo[/yellow]",
151+
title_align="left",
152+
subtitle="[blue]https://github.com/dusktreader/py-buzz[/blue]",
153+
subtitle_align="left",
154+
),
155+
)
156+
console.print(BlankLine)
157+
console.print(BlankLine)
158+
further: bool = Confirm.ask("Would you like to continue?", default=True)
159+
return further

0 commit comments

Comments
 (0)