diff --git a/.buildinfo b/.buildinfo index e9f95df..17657a0 100644 --- a/.buildinfo +++ b/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: bd55037cf58072dab2b4928f34e6396f +config: 53eec1611f49388a3456e46df7aa6f21 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/_modules/index.html b/_modules/index.html index db8ddcd..2d40347 100644 --- a/_modules/index.html +++ b/_modules/index.html @@ -6,7 +6,7 @@
Lightweight Kit Jumpstart
"""
-from lkj.iterables import (
+from lkj.iterables import (
compare_sets, # Compare two iterables and return common, left-only, and right-only elements
index_of, # Get the index of an element in an iterable
get_by_value, # Get a dictionary from a list of dictionaries by a field value
)
-from lkj.funcs import mk_factory
-from lkj.dicts import truncate_dict_values, inclusive_subdict, exclusive_subdict
-from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
-from lkj.strings import (
+from lkj.funcs import mk_factory
+from lkj.dicts import (
+ truncate_dict_values, # Truncate list and string values in a dictionary
+ inclusive_subdict, # new dictionary with only the keys in `include`
+ exclusive_subdict, # new dictionary with only the keys not in `exclude`.
+ merge_dicts, # Merge multiple dictionaries recursively
+)
+from lkj.filesys import get_app_data_dir, get_watermarked_dir, enable_sourcing_from_file
+from lkj.strings import (
indent_lines, # Indent all lines of a string
most_common_indent, # Get the most common indent of a multiline string
regex_based_substitution,
- truncate_string_with_marker, # Truncate a string to a maximum length, inserting a marker in the middle.
+ truncate_string, # Truncate a string to a maximum length, inserting a marker in the middle.
+ truncate_lines, # Truncate a multiline string to a maximum number of lines
unique_affixes, # Get unique prefixes or suffixes of a list of strings
camel_to_snake, # Convert CamelCase to snake_case
snake_to_camel, # Convert snake_case to CamelCase
fields_of_string_format, # Extract field names from a string format
- fields_of_string_formats, # Extract field names from an iterable of string formats
+ fields_of_string_formats, # Extract field names from an iterable of string formats,
+ truncate_string_with_marker, # Deprecated: Backcompatibility alias
)
-from lkj.loggers import (
+from lkj.loggers import (
print_with_timestamp,
print_progress,
log_calls,
@@ -115,16 +122,16 @@ Source code for lkj
return_error_info_on_error,
wrapped_print,
)
-from lkj.importing import import_object, register_namespace_forwarding
-from lkj.chunking import chunk_iterable, chunker
-from lkj.misc import identity, value_in_interval
+from lkj.importing import import_object, register_namespace_forwarding
+from lkj.chunking import chunk_iterable, chunker
+from lkj.misc import identity, value_in_interval
ddir = lambda obj: list(filter(lambda x: not x.startswith('_'), dir(obj)))
[docs]
-def user_machine_id():
+def user_machine_id():
"""Get an ID for the current computer/user that calls this function."""
return __import__('platform').node()
@@ -132,7 +139,7 @@ Source code for lkj
[docs]
-def add_attr(attr_name: str, attr_val: str = None, obj=None):
+def add_attr(attr_name: str, attr_val: str = None, obj=None):
"""Add an attribute to an object.
If no object is provided, return a partial function that takes an object as its
@@ -176,7 +183,7 @@ Source code for lkj
"""
if obj is None:
- from functools import partial
+ from functools import partial
if attr_val is None:
return partial(add_attr, attr_name)
@@ -189,7 +196,7 @@ Source code for lkj
[docs]
-def add_as_attribute_of(obj, name=None):
+def add_as_attribute_of(obj, name=None):
"""Decorator that adds a function as an attribute of a container object ``obj``.
If no ``name`` is given, the ``__name__`` of the function will be used, with a
@@ -244,7 +251,7 @@ Source code for lkj
"""
- def _decorator(f):
+ def _decorator(f):
attrname = name or f.__name__
if not name and attrname.startswith('_'):
attrname = attrname[1:] # remove leading underscore
@@ -257,12 +264,12 @@ Source code for lkj
[docs]
-def get_caller_package_name(default=None):
+def get_caller_package_name(default=None):
"""Return package name of caller
See: https://github.com/i2mint/i2mint/issues/1#issuecomment-1479416085
"""
- import inspect
+ import inspect
try:
stack = inspect.stack()
diff --git a/_modules/lkj/chunking.html b/_modules/lkj/chunking.html
index 64b7d17..e58d81e 100644
--- a/_modules/lkj/chunking.html
+++ b/_modules/lkj/chunking.html
@@ -6,7 +6,7 @@
lkj.chunking — lkj 0.1.10 documentation
-
+
@@ -86,9 +86,9 @@
Source code for lkj.chunking
"""Tools for chunking (segumentation, batching, slicing, etc.)"""
-from itertools import zip_longest, chain, islice
+from itertools import zip_longest, chain, islice
-from typing import (
+from typing import (
Iterable,
Union,
Dict,
@@ -108,7 +108,7 @@ Source code for lkj.chunking
[docs]
-def chunk_iterable(
+def chunk_iterable(
iterable: Union[Iterable[T], Mapping[KT, VT]],
chk_size: int,
*,
@@ -167,7 +167,7 @@ Source code for lkj.chunking
[docs]
-def chunker(
+def chunker(
a: Iterable[T],
chk_size: int,
*,
diff --git a/_modules/lkj/dicts.html b/_modules/lkj/dicts.html
index 157fc2c..f4b0b68 100644
--- a/_modules/lkj/dicts.html
+++ b/_modules/lkj/dicts.html
@@ -6,7 +6,7 @@
lkj.dicts — lkj 0.1.10 documentation
-
+
@@ -92,12 +92,12 @@ Source code for lkj.dicts
"""
-from typing import Optional
+from typing import Optional
[docs]
-def inclusive_subdict(d, include):
+def inclusive_subdict(d, include):
"""
Returns a new dictionary with only the keys in `include`.
@@ -116,7 +116,7 @@ Source code for lkj.dicts
[docs]
-def exclusive_subdict(d, exclude):
+def exclusive_subdict(d, exclude):
"""
Returns a new dictionary with only the keys not in `exclude`.
@@ -127,7 +127,7 @@ Source code for lkj.dicts
Example:
>>> exclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'})
{'b': 2}
-
+
"""
return {k: d[k] for k in d.keys() - exclude}
@@ -136,12 +136,12 @@ Source code for lkj.dicts
# Note: There is a copy of truncate_dict_values in the ju package.
[docs]
-def truncate_dict_values(
+def truncate_dict_values(
d: dict,
*,
max_list_size: Optional[int] = 2,
max_string_size: Optional[int] = 66,
- middle_marker: str = "..."
+ middle_marker: str = "...",
) -> dict:
"""
Returns a new dictionary with the same nested keys structure, where:
@@ -175,7 +175,7 @@ Source code for lkj.dicts
"""
- def truncate_string(value, max_len, marker):
+ def truncate_string(value, max_len, marker):
if max_len is None or len(value) <= max_len:
return value
half_len = (max_len - len(marker)) // 2
@@ -199,6 +199,126 @@ Source code for lkj.dicts
else:
return d
+
+
+from typing import Mapping, Callable, TypeVar, Iterable, Tuple
+
+KT = TypeVar("KT") # Key type
+VT = TypeVar("VT") # Value type
+
+# Note: Could have all function parameters (recursive_condition, etc.) also take the
+# enumerated index of the mapping as an argument. That would give us even more
+# flexibility, but it might be overkill and make the interface more complex.
+from typing import Mapping, Callable, TypeVar, Iterable, Tuple
+from collections import defaultdict
+
+KT = TypeVar("KT") # Key type
+VT = TypeVar("VT") # Value type
+
+
+
+[docs]
+def merge_dicts(
+ *mappings: Mapping[KT, VT],
+ recursive_condition: Callable[[VT], bool] = lambda v: isinstance(v, Mapping),
+ conflict_resolver: Callable[[VT, VT], VT] = lambda x, y: y,
+ mapping_constructor: Callable[[Iterable[Tuple[KT, VT]]], Mapping[KT, VT]] = dict,
+) -> Mapping[KT, VT]:
+ """
+ Merge multiple mappings into a single mapping, recursively if needed,
+ with customizable conflict resolution for non-mapping values.
+
+ This function generalizes the normal `dict.update()` method, which takes the union
+ of the keys and resolves conflicting values by overriding them with the last value.
+ While `dict.update()` performs a single-level merge, `merge_dicts` provides additional
+ flexibility to handle nested mappings. With `merge_dicts`, you can:
+ - Control when to recurse (e.g., based on whether a value is a `Mapping`).
+ - Specify how to resolve value conflicts (e.g., override, add, or accumulate in a list).
+ - Choose the type of mapping (e.g., `dict`, `defaultdict`) to use as the container.
+
+ Args:
+ mappings: The mappings to merge.
+ recursive_condition: A callable to determine if values should be merged recursively.
+ By default, checks if the value is a `Mapping`.
+ conflict_resolver: A callable that resolves conflicts between two values.
+ By default, overrides with the last seen value (`lambda x, y: y`).
+ mapping_constructor: A callable to construct the resulting mapping.
+ Defaults to the standard `dict` constructor.
+
+ Returns:
+ A merged mapping that combines all the input mappings.
+
+ Examples:
+ Basic usage with single-level merge (override behavior):
+ >>> dict1 = {"a": 1}
+ >>> dict2 = {"a": 2, "b": 3}
+ >>> merge_dicts(dict1, dict2)
+ {'a': 2, 'b': 3}
+
+ Handling nested mappings with default behavior (override conflicts):
+ >>> dict1 = {"a": 1, "b": {"x": 10, "y": 20}}
+ >>> dict2 = {"b": {"y": 30, "z": 40}, "c": 3}
+ >>> dict3 = {"b": {"x": 50}, "d": 4}
+ >>> merge_dicts(dict1, dict2, dict3)
+ {'a': 1, 'b': {'x': 50, 'y': 30, 'z': 40}, 'c': 3, 'd': 4}
+
+ Resolving conflicts by summing values:
+ >>> dict1 = {"a": 1}
+ >>> dict2 = {"a": 2}
+ >>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y)
+ {'a': 3}
+
+ Accumulating conflicting values into a list:
+ >>> dict1 = {"a": 1, "b": [1, 2]}
+ >>> dict2 = {"b": [3, 4]}
+ >>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y if isinstance(x, list) else [x, y])
+ {'a': 1, 'b': [1, 2, 3, 4]}
+
+ Recursing only on specific conditions:
+ >>> dict1 = {"a": {"nested": 1}}
+ >>> dict2 = {"a": {"nested": 2, "new": 3}}
+ >>> merge_dicts(dict1, dict2)
+ {'a': {'nested': 2, 'new': 3}}
+
+ >>> dict1 = {"a": {"nested": [1, 2]}}
+ >>> dict2 = {"a": {"nested": [3, 4]}}
+ >>> merge_dicts(dict1, dict2, recursive_condition=lambda v: isinstance(v, dict))
+ {'a': {'nested': [3, 4]}}
+
+ Using a custom mapping type (`defaultdict`):
+ >>> from collections import defaultdict
+ >>> merge_dicts(
+ ... dict1, dict2, mapping_constructor=lambda items: defaultdict(int, items)
+ ... )
+ defaultdict(<class 'int'>, {'a': defaultdict(<class 'int'>, {'nested': [3, 4]})})
+ """
+ # Initialize merged mapping with an empty iterable for constructors requiring input
+ merged = mapping_constructor([])
+
+ for mapping in mappings:
+ for key, value in mapping.items():
+ if (
+ key in merged
+ and recursive_condition(value)
+ and recursive_condition(merged[key])
+ ):
+ # Recursively merge nested mappings
+ merged[key] = merge_dicts(
+ merged[key],
+ value,
+ recursive_condition=recursive_condition,
+ conflict_resolver=conflict_resolver,
+ mapping_constructor=mapping_constructor,
+ )
+ elif key in merged:
+ # Resolve conflict using the provided resolver
+ merged[key] = conflict_resolver(merged[key], value)
+ else:
+ # Otherwise, add the value
+ merged[key] = value
+
+ return merged
+
diff --git a/_modules/lkj/filesys.html b/_modules/lkj/filesys.html
index ed0ebaa..6d0ffb9 100644
--- a/_modules/lkj/filesys.html
+++ b/_modules/lkj/filesys.html
@@ -6,7 +6,7 @@
lkj.filesys — lkj 0.1.10 documentation
-
+
@@ -86,16 +86,16 @@
Source code for lkj.filesys
"""File system utils"""
-import os
-from typing import Callable, Any
-from pathlib import Path
-from functools import wraps, partial
+import os
+from typing import Callable, Any
+from pathlib import Path
+from functools import wraps, partial
# TODO: General pattern Consider generalizing to different conditions and actions
[docs]
-def enable_sourcing_from_file(func=None, *, write_output=False):
+def enable_sourcing_from_file(func=None, *, write_output=False):
"""
Decorator for functions enables the decorated function to source from a file.
@@ -115,7 +115,7 @@ Source code for lkj.filesys
return partial(enable_sourcing_from_file, write_output=write_output)
@wraps(func)
- def wrapper(*args, write_output=write_output, **kwargs):
+ def wrapper(*args, write_output=write_output, **kwargs):
# Check if the first argument is a string and a valid file path
if args and isinstance(args[0], str) and os.path.isfile(args[0]):
file_path = args[0]
@@ -145,13 +145,13 @@ Source code for lkj.filesys
[docs]
-def do_nothing(*args, **kwargs) -> None:
+def do_nothing(*args, **kwargs) -> None:
"""Function that does nothing."""
pass
-def _app_data_rootdir():
+def _app_data_rootdir():
"""
Returns the full path of a directory suitable for storing application-specific data.
@@ -185,7 +185,7 @@ Source code for lkj.filesys
[docs]
-def get_app_data_dir(
+def get_app_data_dir(
dirname='',
*,
if_exists: Callable[[str], Any] = do_nothing,
@@ -261,7 +261,7 @@ Source code for lkj.filesys
[docs]
-def watermark_dir(dirpath: str, watermark: str = DFLT_WATERMARK):
+def watermark_dir(dirpath: str, watermark: str = DFLT_WATERMARK):
"""Watermark."""
(Path(dirpath) / watermark).touch()
@@ -269,13 +269,13 @@ Source code for lkj.filesys
[docs]
-def has_watermark(dirpath: str, watermark: str = DFLT_WATERMARK):
+def has_watermark(dirpath: str, watermark: str = DFLT_WATERMARK):
"""Check if a directory has a watermark."""
return (Path(dirpath) / watermark).exists()
-def _raise_watermark_error(dirpath, watermark):
+def _raise_watermark_error(dirpath, watermark):
raise ValueError(
f'Directory {dirpath} is not watermarked with {watermark}. '
f'Perhaps you deleted the watermark file? If so, create the file and all will '
@@ -286,7 +286,7 @@ Source code for lkj.filesys
[docs]
-def get_watermarked_dir(
+def get_watermarked_dir(
dirname: str,
watermark: str = DFLT_WATERMARK,
*,
@@ -314,11 +314,11 @@ Source code for lkj.filesys
"""
- def create_and_watermark(dirpath):
+ def create_and_watermark(dirpath):
make_dir(dirpath)
watermark_dir(dirpath, watermark=watermark)
- def validate_watermark(dirpath):
+ def validate_watermark(dirpath):
if not has_watermark(dirpath, watermark):
return if_watermark_validation_fails(dirpath, watermark)
@@ -336,7 +336,7 @@ Source code for lkj.filesys
[docs]
-def rename_file(
+def rename_file(
file: Filepath,
renamer_function: Callable[[str], str],
*,
diff --git a/_modules/lkj/funcs.html b/_modules/lkj/funcs.html
index 57ddcc2..9f99f62 100644
--- a/_modules/lkj/funcs.html
+++ b/_modules/lkj/funcs.html
@@ -6,7 +6,7 @@
lkj.funcs — lkj 0.1.10 documentation
-
+
@@ -86,7 +86,7 @@
Source code for lkj.funcs
"""Tools for working with functions."""
-from functools import partial
+from functools import partial
mk_factory = partial(partial, partial)
mk_factory.__doc__ = """
@@ -111,7 +111,7 @@ Source code for lkj.funcs
# when, for example, you want to avoid using a `lambda` function in some situation
# (say, because [lambda functions are not pickle-serializable](https://realpython.com/python-pickle-module/)).
-import operator
+import operator
greater_than = mk_factory(operator.lt)
greater_than.__doc__ = """
@@ -174,7 +174,7 @@ Source code for lkj.funcs
# Imagine working with a large project where you frequently need to generate file paths.
# With mk_factory, creating a path generator becomes effortless.
-import os
+import os
mk_abs_path_maker = mk_factory(os.path.join)
mk_abs_path_maker.__doc__ = """
@@ -250,12 +250,12 @@ Source code for lkj.funcs
#
# Use zipper with an infinite iterator to provide a continuous stream of unique keys to pair with values dynamically.
-from itertools import count
+from itertools import count
[docs]
-def zip_with_filenames(start_idx=1):
+def zip_with_filenames(start_idx=1):
"""
Create a function that zips an iterable with file names like 'file_001', 'file_002', etc.
@@ -279,7 +279,7 @@ Source code for lkj.funcs
#
# For tasks involving regular expressions, mk_factory can simplify creating matchers and extractors.
-import re
+import re
matcher = mk_factory(re.match)
matcher.__doc__ = """
@@ -314,8 +314,8 @@ Source code for lkj.funcs
#
# Reduction functions allow aggregation of elements, useful for calculating products, sums, or even concatenating strings.
-from functools import reduce
-from operator import mul, add
+from functools import reduce
+from operator import mul, add
reducer = mk_factory(reduce)
reducer.__doc__ = """
@@ -359,7 +359,7 @@ Source code for lkj.funcs
# Example using map with os.path.join to create full file paths
[docs]
-def full_path_maker(base_directory):
+def full_path_maker(base_directory):
"""
Create a function that generates full file paths given filenames.
@@ -377,7 +377,7 @@ Source code for lkj.funcs
# Example of using greater_than in a filter
[docs]
-def filter_greater_than(threshold, data):
+def filter_greater_than(threshold, data):
"""
Filter elements in data that are greater than the threshold.
diff --git a/_modules/lkj/importing.html b/_modules/lkj/importing.html
index f220767..16b826e 100644
--- a/_modules/lkj/importing.html
+++ b/_modules/lkj/importing.html
@@ -6,7 +6,7 @@
lkj.importing — lkj 0.1.10 documentation
-
+
@@ -89,7 +89,7 @@ Source code for lkj.importing
[docs]
-def import_object(dot_path: str):
+def import_object(dot_path: str):
"""Imports and returns an object from a dot string path.
>>> f = import_object('os.path.join')
@@ -98,7 +98,7 @@ Source code for lkj.importing
True
"""
- from importlib import import_module
+ from importlib import import_module
module_path, _, object_name = dot_path.rpartition('.')
if not module_path:
@@ -111,14 +111,14 @@ Source code for lkj.importing
-import sys
-import importlib.util
-import importlib.abc
+import sys
+import importlib.util
+import importlib.abc
[docs]
-class NamespaceForwardingLoader(importlib.abc.Loader):
+class NamespaceForwardingLoader(importlib.abc.Loader):
"""
A custom loader that forwards the import from a source namespace to a target namespace.
@@ -132,14 +132,14 @@ Source code for lkj.importing
Loads and returns the target module corresponding to the source module name.
"""
- def __init__(self, fullname, source_base, target_base):
+ def __init__(self, fullname, source_base, target_base):
self.fullname = fullname
self.source_base = source_base
self.target_base = target_base
[docs]
- def load_module(self, fullname):
+ def load_module(self, fullname):
target_name = fullname.replace(self.source_base, self.target_base)
# If the target module is already loaded, return it
@@ -162,7 +162,7 @@ Source code for lkj.importing
[docs]
-class NamespaceForwardingFinder(importlib.abc.MetaPathFinder):
+class NamespaceForwardingFinder(importlib.abc.MetaPathFinder):
"""
A custom finder that detects when a module in the source namespace is being imported
and forwards it to the target namespace.
@@ -176,13 +176,13 @@ Source code for lkj.importing
Finds and returns the spec for the target module corresponding to the source module name.
"""
- def __init__(self, source_base, target_base):
+ def __init__(self, source_base, target_base):
self.source_base = source_base
self.target_base = target_base
[docs]
- def find_spec(self, fullname, path, target=None):
+ def find_spec(self, fullname, path, target=None):
if fullname.startswith(self.source_base):
return importlib.util.spec_from_loader(
fullname,
@@ -195,7 +195,7 @@ Source code for lkj.importing
[docs]
-def register_namespace_forwarding(source_base, target_base):
+def register_namespace_forwarding(source_base, target_base):
"""
Register the namespace forwarding from source_base to target_base.
diff --git a/_modules/lkj/iterables.html b/_modules/lkj/iterables.html
index 2838b44..90dfad0 100644
--- a/_modules/lkj/iterables.html
+++ b/_modules/lkj/iterables.html
@@ -6,7 +6,7 @@
lkj.iterables — lkj 0.1.10 documentation
-
+
@@ -86,12 +86,12 @@
Source code for lkj.iterables
"""Tools with iterables (dicts, lists, tuples, sets, etc.)."""
-from typing import Sequence, Mapping, KT, VT, Iterable, Iterable, NamedTuple
+from typing import Sequence, Mapping, KT, VT, Iterable, Iterable, NamedTuple
[docs]
-class SetsComparisonResult(NamedTuple):
+class SetsComparisonResult(NamedTuple):
common: set
left_only: set
right_only: set
@@ -100,7 +100,7 @@ Source code for lkj.iterables
[docs]
-def compare_sets(left: Iterable, right: Iterable) -> SetsComparisonResult:
+def compare_sets(left: Iterable, right: Iterable) -> SetsComparisonResult:
"""
Compares two iterables and returns a named tuple with:
- Elements in both iterables.
@@ -141,7 +141,7 @@ Source code for lkj.iterables
[docs]
-def index_of(iterable: Iterable[VT], value: VT) -> int:
+def index_of(iterable: Iterable[VT], value: VT) -> int:
"""
List list.index but for any iterable.
@@ -165,7 +165,7 @@ Source code for lkj.iterables
[docs]
-def get_by_value(
+def get_by_value(
list_of_dicts: Sequence[Mapping[KT, VT]], value: VT, field: KT
) -> Mapping[KT, VT]:
"""
diff --git a/_modules/lkj/loggers.html b/_modules/lkj/loggers.html
index 7265d8b..5bfc559 100644
--- a/_modules/lkj/loggers.html
+++ b/_modules/lkj/loggers.html
@@ -6,7 +6,7 @@
lkj.loggers — lkj 0.1.10 documentation
-
+
@@ -86,16 +86,16 @@
Source code for lkj.loggers
"""Utils for logging."""
-from typing import Callable, Tuple, Any, Optional, Union, Iterable
-from functools import partial, wraps
-from operator import attrgetter
+from typing import Callable, Tuple, Any, Optional, Union, Iterable
+from functools import partial, wraps
+from operator import attrgetter
# TODO: Verify and add test for line_prefix
# TODO: Merge with wrap_text_with_exact_spacing
# TODO: Add doctests for string
[docs]
-def wrapped_print(
+def wrapped_print(
items: Union[str, Iterable],
sep=', ',
max_width=80,
@@ -146,7 +146,7 @@ Source code for lkj.loggers
items, max_width=max_width, print_func=print_func, line_prefix=line_prefix
)
else:
- import textwrap
+ import textwrap
return print_func(
line_prefix
@@ -160,7 +160,7 @@ Source code for lkj.loggers
# TODO: Merge with wrapped_print
[docs]
-def wrap_text_with_exact_spacing(
+def wrap_text_with_exact_spacing(
text, *, max_width=80, print_func=print, line_prefix: str = ""
):
"""
@@ -171,7 +171,7 @@ Source code for lkj.loggers
- text (str): The text to wrap and print.
- max_width (int): The maximum width of each line (default is 88).
"""
- import textwrap
+ import textwrap
# Split the text into lines, preserving the existing newlines
lines = text.splitlines(keepends=True)
@@ -197,7 +197,7 @@ Source code for lkj.loggers
[docs]
-def print_with_timestamp(msg, *, refresh=None, display_time=True, print_func=print):
+def print_with_timestamp(msg, *, refresh=None, display_time=True, print_func=print):
"""Prints with a timestamp and optional refresh.
input: message, and possibly args (to be placed in the message string, sprintf-style
@@ -207,9 +207,9 @@ Source code for lkj.loggers
use: To be able to track processes (and the time they take)
"""
- from datetime import datetime
+ from datetime import datetime
- def hms_message(msg=''):
+ def hms_message(msg=''):
t = datetime.now()
return '({:02.0f}){:02.0f}:{:02.0f}:{:02.0f} - {}'.format(
t.day, t.hour, t.minute, t.second, msg
@@ -229,7 +229,7 @@ Source code for lkj.loggers
[docs]
-def clog(condition, *args, log_func=print, **kwargs):
+def clog(condition, *args, log_func=print, **kwargs):
"""Conditional log
>>> clog(False, "logging this")
@@ -255,7 +255,7 @@ Source code for lkj.loggers
"""
if not args and not kwargs:
- import functools
+ import functools
return functools.partial(clog, condition, log_func=log_func)
if condition:
@@ -263,22 +263,22 @@ Source code for lkj.loggers
-def _calling_name(func_name: str, args: Tuple, kwargs: dict) -> str:
+def _calling_name(func_name: str, args: Tuple, kwargs: dict) -> str:
return f"Calling {func_name}..."
-def _done_calling_name(func_name: str, args: Tuple, kwargs: dict, result: Any) -> str:
+def _done_calling_name(func_name: str, args: Tuple, kwargs: dict, result: Any) -> str:
return f".... Done calling {func_name}"
-def _always_log(func: Callable, args: Tuple, kwargs: dict) -> bool:
+def _always_log(func: Callable, args: Tuple, kwargs: dict) -> bool:
"""Return True no matter what"""
return True
[docs]
-def log_calls(
+def log_calls(
func: Callable = None,
*,
logger: Callable[[str], None] = print,
@@ -382,7 +382,7 @@ Source code for lkj.loggers
)
@wraps(func)
- def wrapper(*args, **kwargs):
+ def wrapper(*args, **kwargs):
if log_condition(func, args, kwargs):
if ingress_msg:
logger(ingress_msg(func_name(func), args, kwargs))
@@ -399,7 +399,7 @@ Source code for lkj.loggers
[docs]
-def instance_flag_is_set(func, args, kwargs, flag_attr: str = 'verbose'):
+def instance_flag_is_set(func, args, kwargs, flag_attr: str = 'verbose'):
"""Check if the log flag is set to True in the instance."""
# get the first argument if any, assuming it's the instance
if flag_attr:
@@ -440,18 +440,18 @@ Source code for lkj.loggers
# Error handling
-from typing import Callable, Tuple, Any
-from dataclasses import dataclass
-import traceback
-from typing import Callable, Any, Tuple
-from functools import partial, wraps
-from operator import attrgetter
+from typing import Callable, Tuple, Any
+from dataclasses import dataclass
+import traceback
+from typing import Callable, Any, Tuple
+from functools import partial, wraps
+from operator import attrgetter
[docs]
@dataclass
-class ErrorInfo:
+class ErrorInfo:
func: Callable
error: Exception
traceback: str
@@ -459,13 +459,13 @@ Source code for lkj.loggers
-def _dflt_msg_func(error_info: ErrorInfo) -> str:
+def _dflt_msg_func(error_info: ErrorInfo) -> str:
func_name = getattr(error_info.func, '__name__', 'unknown')
error_obj = error_info.error
return f"Exiting from {func_name} with error: {error_obj}"
-def dflt_error_info_processor(
+def dflt_error_info_processor(
error_info: ErrorInfo,
*,
log_func=print,
@@ -476,7 +476,7 @@ Source code for lkj.loggers
[docs]
-def return_error_info_on_error(
+def return_error_info_on_error(
func,
*,
caught_error_types: Tuple[Exception] = (Exception,),
@@ -514,7 +514,7 @@ Source code for lkj.loggers
"""
@wraps(func)
- def wrapper(*args, **kwargs):
+ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except caught_error_types as error_obj:
diff --git a/_modules/lkj/misc.html b/_modules/lkj/misc.html
index 91e5885..5c4d00b 100644
--- a/_modules/lkj/misc.html
+++ b/_modules/lkj/misc.html
@@ -6,7 +6,7 @@
lkj.misc — lkj 0.1.10 documentation
-
+
@@ -86,18 +86,18 @@
Source code for lkj.misc
"""Miscellaneous tools."""
-from operator import attrgetter, ge, gt, le, lt
-from functools import partial
-from typing import Callable, T, Optional, Any
+from operator import attrgetter, ge, gt, le, lt
+from functools import partial
+from typing import Callable, T, Optional, Any
-def identity(x):
+def identity(x):
return x
[docs]
-def value_in_interval(
+def value_in_interval(
x: Any = None,
/,
*,
diff --git a/_modules/lkj/strings.html b/_modules/lkj/strings.html
index dae53ef..27b8645 100644
--- a/_modules/lkj/strings.html
+++ b/_modules/lkj/strings.html
@@ -6,7 +6,7 @@
lkj.strings — lkj 0.1.10 documentation
-
+
@@ -86,12 +86,12 @@
Source code for lkj.strings
"""Utils for strings"""
-import re
+import re
[docs]
-def indent_lines(string: str, indent: str, *, line_sep='\n') -> str:
+def indent_lines(string: str, indent: str, *, line_sep='\n') -> str:
r"""
Indent each line of a string.
@@ -109,7 +109,7 @@ Source code for lkj.strings
[docs]
-def most_common_indent(string: str, ignore_first_line=False) -> str:
+def most_common_indent(string: str, ignore_first_line=False) -> str:
r"""
Find the most common indentation in a string.
@@ -134,12 +134,12 @@ Source code for lkj.strings
-from string import Formatter
+from string import Formatter
formatter = Formatter()
-def fields_of_string_format(template):
+def fields_of_string_format(template):
return [
field_name for _, field_name, _, _ in formatter.parse(template) if field_name
]
@@ -147,7 +147,7 @@ Source code for lkj.strings
[docs]
-def fields_of_string_formats(templates, *, aggregator=set):
+def fields_of_string_formats(templates, *, aggregator=set):
"""
Extract all unique field names from the templates in _github_url_templates using string.Formatter.
@@ -163,7 +163,7 @@ Source code for lkj.strings
['other', 'that', 'this']
"""
- def field_names():
+ def field_names():
for template in templates:
yield from fields_of_string_format(template)
@@ -171,7 +171,7 @@ Source code for lkj.strings
-import re
+import re
# Compiled regex to handle camel case to snake case conversions, including acronyms
_camel_to_snake_re = re.compile(r'((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))')
@@ -179,7 +179,7 @@ Source code for lkj.strings
[docs]
-def camel_to_snake(camel_string):
+def camel_to_snake(camel_string):
"""
Convert a CamelCase string to snake_case. Useful for converting class
names to variable names.
@@ -209,7 +209,7 @@ Source code for lkj.strings
[docs]
-def snake_to_camel(snake_string):
+def snake_to_camel(snake_string):
"""
Convert a snake_case string to CamelCase. Useful for converting variable
names to class names.
@@ -237,11 +237,9 @@ Source code for lkj.strings
# Note: Vendored in i2.multi_objects and dol.util
-
-[docs]
-def truncate_string_with_marker(
- s, *, left_limit=15, right_limit=15, middle_marker='...'
-):
+
+[docs]
+def truncate_string(s: str, *, left_limit=15, right_limit=15, middle_marker='...'):
"""
Truncate a string to a maximum length, inserting a marker in the middle.
@@ -251,23 +249,23 @@ Source code for lkj.strings
If the string is shorter than the sum of the left_limit and right_limit,
the string is returned as is.
- >>> truncate_string_with_marker('1234567890')
+ >>> truncate_string('1234567890')
'1234567890'
But if the string is longer than the sum of the limits, it is truncated:
- >>> truncate_string_with_marker('1234567890', left_limit=3, right_limit=3)
+ >>> truncate_string('1234567890', left_limit=3, right_limit=3)
'123...890'
- >>> truncate_string_with_marker('1234567890', left_limit=3, right_limit=0)
+ >>> truncate_string('1234567890', left_limit=3, right_limit=0)
'123...'
- >>> truncate_string_with_marker('1234567890', left_limit=0, right_limit=3)
+ >>> truncate_string('1234567890', left_limit=0, right_limit=3)
'...890'
If you're using a specific parametrization of the function often, you can
create a partial function with the desired parameters:
>>> from functools import partial
- >>> truncate_string = partial(truncate_string_with_marker, left_limit=2, right_limit=2, middle_marker='---')
+ >>> truncate_string = partial(truncate_string, left_limit=2, right_limit=2, middle_marker='---')
>>> truncate_string('1234567890')
'12---90'
>>> truncate_string('supercalifragilisticexpialidocious')
@@ -285,10 +283,61 @@ Source code for lkj.strings
+truncate_string_with_marker = truncate_string # backwards compatibility alias
+
+
+
+[docs]
+def truncate_lines(
+ s: str, top_limit: int = None, bottom_limit: int = None, middle_marker: str = '...'
+) -> str:
+ """
+ Truncates a string by limiting the number of lines from the top and bottom.
+ If the total number of lines is greater than top_limit + bottom_limit,
+ it keeps the first `top_limit` lines, keeps the last `bottom_limit` lines,
+ and replaces the omitted middle portion with a single line containing
+ `middle_marker`.
+
+ If top_limit or bottom_limit is None, it is treated as 0.
+
+ Example:
+ >>> text = '''Line1
+ ... Line2
+ ... Line3
+ ... Line4
+ ... Line5
+ ... Line6'''
+
+ >>> print(truncate_lines(text, top_limit=2, bottom_limit=2))
+ Line1
+ Line2
+ ...
+ Line5
+ Line6
+ """
+ # Interpret None as zero for convenience
+ top = top_limit if top_limit is not None else 0
+ bottom = bottom_limit if bottom_limit is not None else 0
+
+ # Split on line boundaries (retaining any trailing newlines in each piece)
+ lines = s.splitlines(True)
+ total_lines = len(lines)
+
+ # If no need to truncate, return as is
+ if total_lines <= top + bottom:
+ return s
+
+ # Otherwise, keep the top lines, keep the bottom lines,
+ # and insert a single marker line in the middle
+ truncated = lines[:top] + [middle_marker + '\n'] + lines[-bottom:]
+ return ''.join(truncated)
+
+
+
# TODO: Generalize so that it can be used with regex keys (not escaped)
[docs]
-def regex_based_substitution(replacements: dict, regex=None, s: str = None):
+def regex_based_substitution(replacements: dict, regex=None, s: str = None):
"""
Construct a substitution function based on an iterable of replacement pairs.
@@ -315,8 +364,8 @@ Source code for lkj.strings
{'banana': 'grape', 'apple': 'orange'}
"""
- import re
- from functools import partial
+ import re
+ from functools import partial
if regex is None and s is None:
# Sort keys by length while maintaining value alignment
@@ -345,23 +394,23 @@ Source code for lkj.strings
-from typing import Callable, Iterable, Sequence
+from typing import Callable, Iterable, Sequence
-class TrieNode:
- def __init__(self):
+class TrieNode:
+ def __init__(self):
self.children = {}
self.count = 0 # Number of times this node is visited during insertion
self.is_end = False # Indicates whether this node represents the end of an item
-def identity(x):
+def identity(x):
return x
[docs]
-def unique_affixes(
+def unique_affixes(
items: Iterable[Sequence],
suffix: bool = False,
*,
@@ -422,12 +471,12 @@ Source code for lkj.strings
if egress is None:
if all(isinstance(item, str) for item in items):
# Items are strings; affixes are lists of characters
- def egress(affix):
+ def egress(affix):
return ''.join(affix)
else:
# Items are sequences (e.g., lists); affixes are lists
- def egress(affix):
+ def egress(affix):
return affix
# If suffix is True, reverse the items
diff --git a/_static/pygments.css b/_static/pygments.css
index 84ab303..6f8b210 100644
--- a/_static/pygments.css
+++ b/_static/pygments.css
@@ -6,9 +6,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */
-.highlight .err { border: 1px solid #FF0000 } /* Error */
+.highlight .err { border: 1px solid #F00 } /* Error */
.highlight .k { color: #008000; font-weight: bold } /* Keyword */
-.highlight .o { color: #666666 } /* Operator */
+.highlight .o { color: #666 } /* Operator */
.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #9C6500 } /* Comment.Preproc */
@@ -25,34 +25,34 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.highlight .gt { color: #0044DD } /* Generic.Traceback */
+.highlight .gt { color: #04D } /* Generic.Traceback */
.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008000 } /* Keyword.Pseudo */
.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #B00040 } /* Keyword.Type */
-.highlight .m { color: #666666 } /* Literal.Number */
+.highlight .m { color: #666 } /* Literal.Number */
.highlight .s { color: #BA2121 } /* Literal.String */
.highlight .na { color: #687822 } /* Name.Attribute */
.highlight .nb { color: #008000 } /* Name.Builtin */
-.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
-.highlight .no { color: #880000 } /* Name.Constant */
-.highlight .nd { color: #AA22FF } /* Name.Decorator */
+.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */
+.highlight .no { color: #800 } /* Name.Constant */
+.highlight .nd { color: #A2F } /* Name.Decorator */
.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
-.highlight .nf { color: #0000FF } /* Name.Function */
+.highlight .nf { color: #00F } /* Name.Function */
.highlight .nl { color: #767600 } /* Name.Label */
-.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
+.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #19177C } /* Name.Variable */
-.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.highlight .w { color: #bbbbbb } /* Text.Whitespace */
-.highlight .mb { color: #666666 } /* Literal.Number.Bin */
-.highlight .mf { color: #666666 } /* Literal.Number.Float */
-.highlight .mh { color: #666666 } /* Literal.Number.Hex */
-.highlight .mi { color: #666666 } /* Literal.Number.Integer */
-.highlight .mo { color: #666666 } /* Literal.Number.Oct */
+.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */
+.highlight .w { color: #BBB } /* Text.Whitespace */
+.highlight .mb { color: #666 } /* Literal.Number.Bin */
+.highlight .mf { color: #666 } /* Literal.Number.Float */
+.highlight .mh { color: #666 } /* Literal.Number.Hex */
+.highlight .mi { color: #666 } /* Literal.Number.Integer */
+.highlight .mo { color: #666 } /* Literal.Number.Oct */
.highlight .sa { color: #BA2121 } /* Literal.String.Affix */
.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
.highlight .sc { color: #BA2121 } /* Literal.String.Char */
@@ -67,9 +67,9 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
.highlight .ss { color: #19177C } /* Literal.String.Symbol */
.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
-.highlight .fm { color: #0000FF } /* Name.Function.Magic */
+.highlight .fm { color: #00F } /* Name.Function.Magic */
.highlight .vc { color: #19177C } /* Name.Variable.Class */
.highlight .vg { color: #19177C } /* Name.Variable.Global */
.highlight .vi { color: #19177C } /* Name.Variable.Instance */
.highlight .vm { color: #19177C } /* Name.Variable.Magic */
-.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
\ No newline at end of file
+.highlight .il { color: #666 } /* Literal.Number.Integer.Long */
\ No newline at end of file
diff --git a/genindex.html b/genindex.html
index 46216f0..2f6bbc4 100644
--- a/genindex.html
+++ b/genindex.html
@@ -6,7 +6,7 @@
Index — lkj 0.1.10 documentation
-
+
@@ -307,6 +307,8 @@ L
M
+ - merge_dicts() (in module lkj.dicts)
+
-
module
@@ -408,9 +410,13 @@
T
- (lkj.importing.NamespaceForwardingLoader attribute)
+ truncate_dict_values() (in module lkj.dicts)
+
- - truncate_dict_values() (in module lkj.dicts)
+
- truncate_lines() (in module lkj.strings)
+
+ - truncate_string() (in module lkj.strings)
- truncate_string_with_marker() (in module lkj.strings)
diff --git a/index.html b/index.html
index 7c9778a..af9f22f 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
Welcome to lkj’s documentation! — lkj 0.1.10 documentation
-
+
@@ -104,6 +104,7 @@ Welcome to lkj’s documentation!lkj.dicts
@@ -160,6 +161,8 @@ Welcome to lkj’s documentation!most_common_indent()
regex_based_substitution()
snake_to_camel()
+truncate_lines()
+truncate_string()
truncate_string_with_marker()
unique_affixes()
@@ -175,7 +178,7 @@ Indices and tablesSearch Page
Release: 0.1.10
-Last change: Dec 18, 2024
+Last change: Jan 30, 2025
diff --git a/module_docs/lkj.html b/module_docs/lkj.html
index 4dc762f..0bc076c 100644
--- a/module_docs/lkj.html
+++ b/module_docs/lkj.html
@@ -7,7 +7,7 @@
lkj — lkj 0.1.10 documentation
-
+
@@ -102,11 +102,11 @@
leading underscore removed. This is useful for adding helper functions to main
“container” functions without polluting the namespace of the module, at least
from the point of view of imports and tab completion.
->>> def foo():
+>>> def foo():
... pass
>>>
>>> @add_as_attribute_of(foo)
-... def helper():
+... def helper():
... pass
>>> hasattr(foo, 'helper')
True
@@ -119,7 +119,7 @@
Note that if the name of the function starts with an underscore, it will be removed
before adding it as an attribute of obj
.
>>> @add_as_attribute_of(foo)
-... def _helper():
+... def _helper():
... pass
>>> hasattr(foo, 'helper')
True
@@ -130,7 +130,7 @@
and tab completion. But if you really want to add a function with a leading
underscore, you can do so by specifying the name explicitly:
>>> @add_as_attribute_of(foo, name='_helper')
-... def _helper():
+... def _helper():
... pass
>>> hasattr(foo, '_helper')
True
@@ -138,7 +138,7 @@
Of course, you can give any name you want to the attribute:
>>> @add_as_attribute_of(foo, name='bar')
-... def _helper():
+... def _helper():
... pass
>>> hasattr(foo, 'bar')
True
@@ -179,7 +179,7 @@
object and/or an attribute value as its argument(s).
->>> def generic_func(*args, **kwargs):
+>>> def generic_func(*args, **kwargs):
... return args, kwargs
...
>>> generic_func.__name__
@@ -195,7 +195,7 @@
>>>
>>> @add_name('my_func')
... @add_doc('This is my function.')
-... def f(*args, **kwargs):
+... def f(*args, **kwargs):
... return args, kwargs
...
>>> f.__name__
diff --git a/module_docs/lkj/chunking.html b/module_docs/lkj/chunking.html
index d7fa640..160b8ac 100644
--- a/module_docs/lkj/chunking.html
+++ b/module_docs/lkj/chunking.html
@@ -7,7 +7,7 @@
lkj.chunking — lkj 0.1.10 documentation
-
+
diff --git a/module_docs/lkj/dicts.html b/module_docs/lkj/dicts.html
index a5ca831..ab4bcb4 100644
--- a/module_docs/lkj/dicts.html
+++ b/module_docs/lkj/dicts.html
@@ -7,7 +7,7 @@
lkj.dicts — lkj 0.1.10 documentation
-
+
@@ -54,6 +54,7 @@
lkj.dicts
@@ -119,6 +120,75 @@
{‘a’: 1, ‘c’: 3}
+
+-
+lkj.dicts.merge_dicts(*mappings: ~typing.Mapping[~lkj.dicts.KT, ~lkj.dicts.VT], recursive_condition: ~typing.Callable[[~lkj.dicts.VT], bool] = <function <lambda>>, conflict_resolver: ~typing.Callable[[~lkj.dicts.VT, ~lkj.dicts.VT], ~lkj.dicts.VT] = <function <lambda>>, mapping_constructor: ~typing.Callable[[~typing.Iterable[~typing.Tuple[~lkj.dicts.KT, ~lkj.dicts.VT]]], ~typing.Mapping[~lkj.dicts.KT, ~lkj.dicts.VT]] = <class 'dict'>) Mapping[KT, VT] [source]
+Merge multiple mappings into a single mapping, recursively if needed,
+with customizable conflict resolution for non-mapping values.
+This function generalizes the normal dict.update() method, which takes the union
+of the keys and resolves conflicting values by overriding them with the last value.
+While dict.update() performs a single-level merge, merge_dicts provides additional
+flexibility to handle nested mappings. With merge_dicts, you can:
+- Control when to recurse (e.g., based on whether a value is a Mapping).
+- Specify how to resolve value conflicts (e.g., override, add, or accumulate in a list).
+- Choose the type of mapping (e.g., dict, defaultdict) to use as the container.
+
+- Parameters:
+
+mappings – The mappings to merge.
+recursive_condition – A callable to determine if values should be merged recursively.
+By default, checks if the value is a Mapping.
+conflict_resolver – A callable that resolves conflicts between two values.
+By default, overrides with the last seen value (lambda x, y: y).
+mapping_constructor – A callable to construct the resulting mapping.
+Defaults to the standard dict constructor.
+
+
+- Returns:
+A merged mapping that combines all the input mappings.
+
+
+Examples
+Basic usage with single-level merge (override behavior):
+>>> dict1 = {“a”: 1}
+>>> dict2 = {“a”: 2, “b”: 3}
+>>> merge_dicts(dict1, dict2)
+{‘a’: 2, ‘b’: 3}
+Handling nested mappings with default behavior (override conflicts):
+>>> dict1 = {“a”: 1, “b”: {“x”: 10, “y”: 20}}
+>>> dict2 = {“b”: {“y”: 30, “z”: 40}, “c”: 3}
+>>> dict3 = {“b”: {“x”: 50}, “d”: 4}
+>>> merge_dicts(dict1, dict2, dict3)
+{‘a’: 1, ‘b’: {‘x’: 50, ‘y’: 30, ‘z’: 40}, ‘c’: 3, ‘d’: 4}
+Resolving conflicts by summing values:
+>>> dict1 = {“a”: 1}
+>>> dict2 = {“a”: 2}
+>>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y)
+{‘a’: 3}
+Accumulating conflicting values into a list:
+>>> dict1 = {“a”: 1, “b”: [1, 2]}
+>>> dict2 = {“b”: [3, 4]}
+>>> merge_dicts(dict1, dict2, conflict_resolver=lambda x, y: x + y if isinstance(x, list) else [x, y])
+{‘a’: 1, ‘b’: [1, 2, 3, 4]}
+Recursing only on specific conditions:
+>>> dict1 = {“a”: {“nested”: 1}}
+>>> dict2 = {“a”: {“nested”: 2, “new”: 3}}
+>>> merge_dicts(dict1, dict2)
+{‘a’: {‘nested’: 2, ‘new’: 3}}
+>>> dict1 = {"a": {"nested": [1, 2]}}
+>>> dict2 = {"a": {"nested": [3, 4]}}
+>>> merge_dicts(dict1, dict2, recursive_condition=lambda v: isinstance(v, dict))
+{'a': {'nested': [3, 4]}}
+
+
+Using a custom mapping type (defaultdict):
+>>> from collections import defaultdict
+>>> merge_dicts(
+… dict1, dict2, mapping_constructor=lambda items: defaultdict(int, items)
+… )
+defaultdict(<class ‘int’>, {‘a’: defaultdict(<class ‘int’>, {‘nested’: [3, 4]})})
+
+
-
lkj.dicts.truncate_dict_values(d: dict, *, max_list_size: int | None = 2, max_string_size: int | None = 66, middle_marker: str = '...') dict [source]
diff --git a/module_docs/lkj/filesys.html b/module_docs/lkj/filesys.html
index 9e64021..06fe64b 100644
--- a/module_docs/lkj/filesys.html
+++ b/module_docs/lkj/filesys.html
@@ -7,7 +7,7 @@
lkj.filesys — lkj 0.1.10 documentation
-
+
@@ -164,8 +164,8 @@
You can control what happens if the directory already exists, or if it doesn’t.
The callbacks take the full path of the directory as an argument, and usually return
the path after doing something with it.
->>> import os
->>> def notify_user_that_path_does_not_exist(path):
+>>> import os
+>>> def notify_user_that_path_does_not_exist(path):
... print(f"The '{os.path.basename(path)}' subdirectory doesn't exist")
... return path
>>> dirpath = get_app_data_dir(
@@ -183,8 +183,8 @@
-
lkj.filesys.get_watermarked_dir(dirname: str, watermark: str = '.lkj', *, if_watermark_validation_fails: ~typing.Callable[[str, str], ~typing.Any] = <function _raise_watermark_error>, make_dir: ~typing.Callable[[str], ~typing.Any] = <built-in function mkdir>, rootdir: str = '/home/runner/.config/')[source]
Get a watermarked directory.
->>> from functools import partial
->>> import tempfile, os, shutil
+>>> from functools import partial
+>>> import tempfile, os, shutil
>>> testdir = os.path.join(tempfile.gettempdir(), 'watermark_testdir')
>>> shutil.rmtree(testdir, ignore_errors=True) # delete
>>> os.makedirs(testdir, exist_ok=True) # and recreate afresh
diff --git a/module_docs/lkj/funcs.html b/module_docs/lkj/funcs.html
index a43c137..c669d05 100644
--- a/module_docs/lkj/funcs.html
+++ b/module_docs/lkj/funcs.html
@@ -7,7 +7,7 @@
lkj.funcs — lkj 0.1.10 documentation
-
+
diff --git a/module_docs/lkj/importing.html b/module_docs/lkj/importing.html
index 6815bf5..0416de3 100644
--- a/module_docs/lkj/importing.html
+++ b/module_docs/lkj/importing.html
@@ -7,7 +7,7 @@
lkj.importing — lkj 0.1.10 documentation
-
+
@@ -203,7 +203,7 @@
lkj.importing.import_object(dot_path: str)[source]
Imports and returns an object from a dot string path.
>>> f = import_object('os.path.join')
->>> from os.path import join
+>>> from os.path import join
>>> f is join
True
@@ -227,7 +227,7 @@
# if you put this code in the imbed.mdat package (say, containing a hcp module),
>>> register_namespace_forwarding(‘imbed.mdat’, ‘imbed_data_prep’) # doctest: +SKIP
# Then when you do
->>> import imbed.mdat.hcp
+>>> import imbed.mdat.hcp
You’ll get the imbed_data_prep.hcp module.
diff --git a/module_docs/lkj/iterables.html b/module_docs/lkj/iterables.html
index cc1b751..e3eb790 100644
--- a/module_docs/lkj/iterables.html
+++ b/module_docs/lkj/iterables.html
@@ -7,7 +7,7 @@
lkj.iterables — lkj 0.1.10 documentation
-
+
@@ -169,7 +169,7 @@
This function just WANTS to be functools.partial-ized!!
->>> from functools import partial
+>>> from functools import partial
>>> get_by_id = partial(get_by_value, field='id')
>>> get_by_id(data, 1)
{'id': 1, 'value': 'A'}
diff --git a/module_docs/lkj/loggers.html b/module_docs/lkj/loggers.html
index 54b2b26..5826fb9 100644
--- a/module_docs/lkj/loggers.html
+++ b/module_docs/lkj/loggers.html
@@ -7,7 +7,7 @@
lkj.loggers — lkj 0.1.10 documentation
-
+
@@ -173,7 +173,7 @@
Example:
>>> @log_calls
-... def add(a, b):
+... def add(a, b):
... return a + b
...
>>> add(2, 3)
@@ -187,7 +187,7 @@
... ingress_msg=lambda name, *args: f"Start {name}!",
... egress_msg=lambda *args: "End"
... )
-... def multiply(a, b):
+... def multiply(a, b):
... return a * b
...
>>> multiply(2, 3)
@@ -201,7 +201,7 @@
One common use case is to log only if a flag is set in an instance.
Since this is a common use case, we provide the log_calls.instance_flag_is_set
helper function for this. You can use partial to set the flag attribute:
->>> import functools
+>>> import functools
>>> log_if_verbose_set_to_true = functools.partial(
... log_calls.instance_flag_is_set, flag_attr='verbose'
... )
@@ -209,12 +209,12 @@
Now if you have a class with a verbose attribute, you can use this helper function
to log only if verbose is set:
->>> class MyClass:
-... def __init__(self, verbose=False):
+>>> class MyClass:
+... def __init__(self, verbose=False):
... self.verbose = verbose
...
... @log_calls(log_condition=log_if_verbose_set_to_true)
-... def foo(self):
+... def foo(self):
... print("Executing foo")
...
>>> # Example usage
@@ -269,7 +269,7 @@
Tip: You can have your error_info_processor persist the error info to a file or
database, or send it to a logging service.
>>> @return_error_info_on_error
-... def foo(x, y=2):
+... def foo(x, y=2):
... return x / y
...
>>> t = foo(1, 2)
diff --git a/module_docs/lkj/misc.html b/module_docs/lkj/misc.html
index d4e743b..dcdaa6f 100644
--- a/module_docs/lkj/misc.html
+++ b/module_docs/lkj/misc.html
@@ -7,7 +7,7 @@
lkj.misc — lkj 0.1.10 documentation
-
+
@@ -94,7 +94,7 @@
-
lkj.misc.value_in_interval(x: ~typing.Any | None = None, /, *, get_val: ~typing.Callable[[~typing.Any], T] = <function identity>, min_val: T | None = None, max_val: T | None = None, is_minimum: ~typing.Callable[[T, T], bool] = <built-in function ge>, is_maximum: ~typing.Callable[[T, T], bool] = <built-in function lt>)[source]
->>> from operator import itemgetter, le
+
>>> from operator import itemgetter, le
>>> f = value_in_interval(get_val=itemgetter('date'), min_val=2, max_val=8)
>>> d = [{'date': 1}, {'date': 2}, {'date': 3, 'x': 7}, {'date': 8}, {'date': 9}]
>>> list(map(f, d))
diff --git a/module_docs/lkj/strings.html b/module_docs/lkj/strings.html
index af48fbe..0bf6904 100644
--- a/module_docs/lkj/strings.html
+++ b/module_docs/lkj/strings.html
@@ -7,7 +7,7 @@
lkj.strings — lkj 0.1.10 documentation
-
+
@@ -64,6 +64,8 @@
most_common_indent()
regex_based_substitution()
snake_to_camel()
+truncate_lines()
+truncate_string()
truncate_string_with_marker()
unique_affixes()
@@ -263,31 +265,92 @@
+
+-
+lkj.strings.truncate_lines(s: str, top_limit: int | None = None, bottom_limit: int | None = None, middle_marker: str = '...') str [source]
+Truncates a string by limiting the number of lines from the top and bottom.
+If the total number of lines is greater than top_limit + bottom_limit,
+it keeps the first top_limit lines, keeps the last bottom_limit lines,
+and replaces the omitted middle portion with a single line containing
+middle_marker.
+If top_limit or bottom_limit is None, it is treated as 0.
+Example
+>>> text = '''Line1
+... Line2
+... Line3
+... Line4
+... Line5
+... Line6'''
+
+
+>>> print(truncate_lines(text, top_limit=2, bottom_limit=2))
+Line1
+Line2
+...
+Line5
+Line6
+
+
+
+
+
+-
+lkj.strings.truncate_string(s: str, *, left_limit=15, right_limit=15, middle_marker='...')[source]
+Truncate a string to a maximum length, inserting a marker in the middle.
+If the string is longer than the sum of the left_limit and right_limit,
+the string is truncated and the middle_marker is inserted in the middle.
+If the string is shorter than the sum of the left_limit and right_limit,
+the string is returned as is.
+>>> truncate_string('1234567890')
+'1234567890'
+
+
+But if the string is longer than the sum of the limits, it is truncated:
+>>> truncate_string('1234567890', left_limit=3, right_limit=3)
+'123...890'
+>>> truncate_string('1234567890', left_limit=3, right_limit=0)
+'123...'
+>>> truncate_string('1234567890', left_limit=0, right_limit=3)
+'...890'
+
+
+If you’re using a specific parametrization of the function often, you can
+create a partial function with the desired parameters:
+>>> from functools import partial
+>>> truncate_string = partial(truncate_string, left_limit=2, right_limit=2, middle_marker='---')
+>>> truncate_string('1234567890')
+'12---90'
+>>> truncate_string('supercalifragilisticexpialidocious')
+'su---us'
+
+
+
+
-
-lkj.strings.truncate_string_with_marker(s, *, left_limit=15, right_limit=15, middle_marker='...')[source]
+lkj.strings.truncate_string_with_marker(s: str, *, left_limit=15, right_limit=15, middle_marker='...')
Truncate a string to a maximum length, inserting a marker in the middle.
If the string is longer than the sum of the left_limit and right_limit,
the string is truncated and the middle_marker is inserted in the middle.
If the string is shorter than the sum of the left_limit and right_limit,
the string is returned as is.
->>> truncate_string_with_marker('1234567890')
+>>> truncate_string('1234567890')
'1234567890'
But if the string is longer than the sum of the limits, it is truncated:
->>> truncate_string_with_marker('1234567890', left_limit=3, right_limit=3)
+