Skip to content

Commit 47322e9

Browse files
committed
add python content and ruff.toml
fixup
1 parent 04efe7e commit 47322e9

11 files changed

+170
-3
lines changed

assets/style.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
--font_size_h3: 100%;
3131

3232
--content_width: 1200px; /* same as @media screen */
33-
--content_1colof2_width: calc(1200px / 2);
33+
--content_1colof2_width: calc(var(--content_width) / 2);
3434
}
3535

3636
body {
@@ -161,7 +161,7 @@ code.zig {
161161
/* .home_right { flex: 1; width: 300px; } */
162162
.home_left { flex: 1; }
163163
.home_right { flex: 1; }
164-
/* SHENNANIGAN: Variables break block view on Mozilla Firefox 132.0.2, so use 1200px:
164+
/* SHENNANIGAN: Variables break block view on Mozilla Firefox 132.0.2, so use 1200px: */
165165
/* @media screen and (max-width: var(--content_width)) { .home_page { display: block; } } */
166166
/* @media screen and (max-width: var(--content_width)) { .static_grid { display: block; } } */
167167
@media screen and (max-width: 1200px) { .home_page { display: block; } }

content/articles/shennanigans_in_python.smd

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,68 @@
55
.layout = "article.shtml",
66
.tags = ["article", "python", "software", "design"],
77
.draft = false,
8-
---
8+
---
9+
10+
[]($section.id("intro"))
11+
The following shennanigans were collected during my work on creating a system
12+
integration library and framework for hardware and software tests including
13+
developer tasks like worktree management, building, target deployment and
14+
configuration. Therefore it should be representative for things one
15+
might want to do in absence of better tooling. The used Python version was
16+
3.8.2, but most problems still persist.
17+
18+
[]($section.id("tldr"))
19+
My [**tldr;**](#tldr) retrospection upfront, which you may be able to reproduce
20+
once you try to code long-running services, which recover from all failures,
21+
cleanly reset state or generally try to debug spurious problems. Please don't.
22+
23+
Python on itself is great for prototyping up to a few hundred lines of code,
24+
like to quickly receive or send some json over tcp/html.
25+
**However**, it is unfeasible to scale, for example to use as library code.
26+
Changes in a leaf function can add exceptions to higher level code paths and
27+
handling those via exceptions for user friendly error messages, for example
28+
to collect context (information along multiple functions, for example from
29+
different combination of traversal) becomes unreasonably verbose and error
30+
prone. The alternative is to use C-like error handling, which requires to figure out
31+
all possible exceptions of Python libstd methods, which language servers
32+
do not support (as of 20240404).
33+
34+
[]($section.id("list_of_shame"))
35+
Aside of these more fundamental limitations, here the [list of shennanigans](#list_of_shennanigans) I have run into:
36+
- `xml.dom.minidom` [breaks space and newlines](https://bugs.python.org/issue5752). Use `ElementTree`.
37+
- `.strip()` is necessary after file read, because Python automatically adds
38+
`\n` and there is no way to read without newlines into a list.
39+
- Testing for subdictionaries with `dict` is unreadable, so such a method is missing
40+
[missing_dict_methods1.py]($code.asset('./missing_dict_methods1.py').language('python'))
41+
- `dict` has no method to check, if the fields of a dictionary are in another dictionary
42+
[missing_dict_methods2.py]($code.asset('./missing_dict_methods2.py').language('python'))
43+
- `dict` has no method to check, if all fields and values of a dictionary are in another dictionary
44+
[missing_dict_methods3.py]($code.asset('./missing_dict_methods3.py').language('python'))
45+
- Tuples and dicts are annoying to differentiate
46+
[tup_and_dicts.py]($code.asset('./tup_and_dicts.py').language('python'))
47+
- Stack trace formatting is inefficient and one can not use <code>gf</code> or <code>gF</code> vim shortcuts to jump to location
48+
function to write status + trace to variable.
49+
[stacktrace_fmt.py]($code.asset('./stacktrace_fmt.py').language('python'))
50+
- Mixed double quote (`"`) and single quote (`'`) strings are invalid json
51+
[invalid_json.py]($code.asset('./invalid_json.py').language('python'))
52+
- `os.kill()` does not call registered cleanup function
53+
`atexit.register(exit_cleanup)` by deamonized threads. Must store pids of
54+
child processes and clean them explicitly or signal main thread via
55+
[signal_main_thread.py]($code.asset('./signal_main_thread.py').language('python'))
56+
- [Socket timeout can cause file-like readline() method to lose data](https://github.com/python/cpython/issues/51571), workaround
57+
1. Read from Kernel structure and append chunk-wise to buffer from socket until stop event (via select).
58+
2. After each read, try to line a line from the buffer and remove the line on success (being utf-8 etc).
59+
3. On failure of reading line, continue with 1.
60+
4. Teardown should read socket until being empty, if a stop was obtained.
61+
- Generic module annotation is not allowed and mypy has no explicit docs for
62+
this. The following does not work and `module: object` is the closest we can
63+
get as simple annotation.
64+
[module_annotation.py]($code.asset('./module_annotation.py').language('python'))
65+
- There are no scheduling and watchdog methods, which makes Python thread
66+
scheduling very unreliable. Unlucky schedules may cause fatal delay for
67+
shuffling data between daemon thread and main thread. As example, an
68+
application using 1 main thread and 2 deamon threads may cause the relevant
69+
deamon thread not being scheduled for 2 seconds.
70+
Empirically 3 seconds work.
71+
- Trailing comma in dictionary or `json.dumps` generated string has
72+
silent failures, for example on parsing the output as json via php.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import json
2+
3+
4+
# Dict -> str is inconsistent to json -> str, so workaround with
5+
# dict_asjson_lower = str(dict1).replace("'", '"')
6+
def combineDictsFromStr() -> None:
7+
dict1 = {"t1": "val1", "t2arr": [{"t2_int": 0, "t2_str": "12.0"}], "t3int": 30}
8+
dict1_str_raw = str(dict1)
9+
dict1_str = dict1_str_raw.replace("'", '"')
10+
dict2_str = '{"anotherone":"yes", '
11+
dict2_str += '"t3int":30,"t4str":'
12+
dict2_str += dict1_str + "}"
13+
dict2 = json.loads(dict2_str)
14+
_ = dict2
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def is_subdict(small: dict, big: dict) -> bool:
2+
"""
3+
Test, if 'small' is subdict of 'big'
4+
Example: big = {'pl' : 'key1': {'key2': 'value2'}}
5+
Then small = {'pl' : 'key1': {'key2': 'value2'}, 'otherkey'..} matches,
6+
but small = {'pl' : 'key1': {'key2': 'value2', 'otherkey'..}}
7+
or small = {'pl' : 'key1': {'key2': {'value2', 'otherkey'..}}} not.
8+
"""
9+
# since python3.9:
10+
# return big | small == big
11+
# also:
12+
# return {**big, **small} == big
13+
return dict(big, **small) == big
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
def has_fieldsvals(small: dict, big: dict) -> bool:
2+
"""
3+
Test, if 'small' has all values of of 'big'
4+
Example: big = {'pl' : 'key1': {'key2': 'value2'}}
5+
Then small = {'pl' : 'key1': {'key2': 'value2'}, 'otherkey'..} matches,
6+
small = {'pl' : 'key1': {'key2': 'value2', 'otherkey'..}} matches,
7+
and small = {'pl' : 'key1': {'key2': {'value2', 'otherkey'..}}} matches.
8+
"""
9+
for key, value in small.items():
10+
if key in big:
11+
if isinstance(small[key], dict):
12+
if not has_fieldsvals(small[key], big[key]):
13+
return False
14+
else:
15+
return True
16+
elif value != big[key]:
17+
return False
18+
else:
19+
return True
20+
else:
21+
return False
22+
return True
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import copy
2+
from typing import Optional, List
3+
4+
5+
def merge_dicts(alpha: dict = {}, beta: dict = {}) -> dict:
6+
"""
7+
Recursive merge dicts. Not multi-threading safe.
8+
"""
9+
return _merge_dicts_aux(alpha, beta, copy.copy(alpha))
10+
11+
12+
def _merge_dicts_aux(alpha: dict = {}, beta: dict = {}, result: dict = {}, path: Optional[List[str]] = None) -> dict:
13+
if path is None:
14+
path = []
15+
for key in beta:
16+
if key not in alpha:
17+
result[key] = beta[key]
18+
else:
19+
if isinstance(alpha[key], dict) and isinstance(beta[key], dict):
20+
# key value is dict in A and B => merge the dicts
21+
_merge_dicts_aux(alpha[key], beta[key], result[key], path + [str(key)])
22+
elif alpha[key] == beta[key]:
23+
# key value is same in A and B => ignore
24+
pass
25+
else:
26+
# key value differs in A and B => raise error
27+
err: str = f"Conflict at {'.'.join(path + [str(key)])}"
28+
raise Exception(err)
29+
return result
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# using 'module: ModuleType' via specifying ModuleType as set of types not possible
2+
def check_fn(module: object) -> int:
3+
if str(type(module)) != "module":
4+
return 1
5+
return 0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def signalMainThread(self) -> None:
2+
pass
3+
# before Python 3.10: _thread.interrupt_main()
4+
# since Python 3.10: _thread.interrupt_main(signum=signal.SIGKILL)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import traceback
2+
3+
4+
def getStackTrace() -> str:
5+
return repr(traceback.format_stack())
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# dictionary
2+
dict1 = {"m1": "cp", "m2": "cp"}
3+
# tuple
4+
tup1 = ({"m1": "cp", "m2": "cp"},)
5+
6+
# at least getting the intention correct, but python is still unhelpful with error message
7+
dict2 = dict({"m1": "cp", "m2": "cp"})
8+
# tuple
9+
tup2 = (tuple({"m1": "cp", "m2": "cp"}),)

0 commit comments

Comments
 (0)