# index.html.md

<!-- generated by epythet -->

# lkj

Lightweight Kit Jumpstart. A place for useful python utils built only with pure python.

To install:	`pip install lkj`

[Documentation](https://i2mint.github.io/lkj)

Note: None of the tools here require anything else but pure python.
Additionally, things are organized in such a way that these can be
easily copy-pasted into other projects.
That is, modules are all self contained (so can easily be copy-paste-vendored
(do be nice and mention the source!))
Further, many functions will contain their own imports: Those functions can even be
copy-paste-vendored by just copying the function body.

# Examples of utils

## Find and replace

`FindReplaceTool` is a general-purpose find-and-replace tool that can treat the input text as a continuous sequence of characters,
even if operations such as viewing context are performed line by line.

The basic usage is

```python
FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
```

```none
Match 0 (around line 1):
apple banana apple
^^^^^
----------------------------------------
Match 1 (around line 1):
apple banana apple
             ^^^^^
----------------------------------------
```

```python
FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
```

```none
'orange banana orange'
```

[See more examples in documentation](https://i2mint.github.io/lkj/module_docs/lkj/strings.html#lkj.strings.FindReplaceTool)

[See here a example of how I used this to edit my CI yamls](https://github.com/i2mint/lkj/discussions/4#discussioncomment-12104547)

## loggers

### clog

Conditional log

```python
>>> clog(False, "logging this")
>>> clog(True, "logging this")
logging this
```

One common usage is when there’s a verbose flag that allows the user to specify
whether they want to log or not. Instead of having to litter your code with
`if verbose:` statements you can just do this:

```python
>>> verbose = True  # say versbose is True
>>> _clog = clog(verbose)  # makes a clog with a fixed condition
>>> _clog("logging this")
logging this
```

You can also choose a different log function.
Usually you’d want to use a logger object from the logging module,
but for this example we’ll just use `print` with some modification:

```python
>>> _clog = clog(verbose, log_func=lambda x: print(f"hello {x}"))
>>> _clog("logging this")
hello logging this
```

### print_with_timestamp

Prints with a timestamp and optional refresh.

- input: message, and possibly args (to be placed in the message string, sprintf-style
- output: Displays the time (HH:MM:SS), and the message
- use: To be able to track processes (and the time they take)

```python
>>> print_with_timestamp('processing element X')
(29)09:56:36 - processing element X
```

### return_error_info_on_error

Decorator that returns traceback and local variables on error.

This decorator is useful for debugging. It will catch any exceptions that occur
in the decorated function, and return an ErrorInfo object with the traceback and
local variables at the time of the error.

- `func`: The function to decorate.
- `caught_error_types`: The types of errors to catch.
- `error_info_processor`: A function that processes the ErrorInfo object.

Tip: To parametrize this decorator, you can use a functools.partial function.

Tip: You can have your error_info_processor persist the error info to a file or
database, or send it to a logging service.

```python
>>> from lkj import return_error_info_on_error, ErrorInfo
>>> @return_error_info_on_error
... def foo(x, y=2):
...     return x / y
...
>>> t = foo(1, 2)
>>> assert t == 0.5
>>> t = foo(1, y=0)
Exiting from foo with error: division by zero
>>> if isinstance(t, ErrorInfo):
...     assert isinstance(t.error, ZeroDivisionError)
...     hasattr(t, 'traceback')
...     assert t.locals['args'] == (1,)
...     assert t.locals['kwargs'] == {'y': 0}
```

## Miscellaneous

### chunker

Chunk an iterable into non-overlapping chunks of size chk_size.

```python
chunker(a, chk_size, *, include_tail=True)
```

```python
>>> from lkj import chunker
>>> list(chunker(range(8), 3))
[(0, 1, 2), (3, 4, 5), (6, 7)]
>>> list(chunker(range(8), 3, include_tail=False))
[(0, 1, 2), (3, 4, 5)]
```

### import_object

Import and return an object from a dot string path.

```python
import_object(dot_path: str)
```

```python
>>> f = import_object('os.path.join')
>>> from os.path import join
>>> f is join
True
```

## Pretty Printing Lists

The `print_list` function provides flexible, human-friendly ways to display lists and collections. It supports multiple display styles and can be used in several ways.

### Basic Usage

```python
from lkj.strings import print_list

items = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig']

# Different display styles
print_list(items, style='wrapped')      # Automatic line wrapping
print_list(items, style='columns')      # Column format
print_list(items, style='numbered')     # Numbered list
print_list(items, style='bullet')       # Bullet points
print_list(items, style='compact')      # All on one line
print_list(items, style='table')        # Table format
```

### Direct Usage with Customization

```python
# Customize width, separators, and formatting
print_list(items, style='wrapped', max_width=40, sep=' | ')
print_list(items, style='columns', items_per_line=3)
print_list(items, style='numbered', line_prefix='  ')
print_list(items, style='bullet', show_count=False)

# Return string instead of printing
result = print_list(items, style='numbered', print_func=None)
print(result)
```

### Partial Function Factory

When you don’t specify the `items` parameter, `print_list` returns a partial function that you can reuse:

```python
# Create specialized printers
numbered_printer = print_list(style='numbered', show_count=False)
bullet_printer = print_list(style='bullet', print_func=None)
compact_printer = print_list(style='compact', max_width=60)

# Reuse with different data
numbered_printer(['a', 'b', 'c'])        # Prints: 1. a\n2. b\n3. c
result = bullet_printer(['x', 'y', 'z']) # Returns: '• x\n• y\n• z'
compact_printer(['item1', 'item2'])      # Prints: item1, item2
```

### Convenience Methods

The `print_list` object provides convenient pre-configured methods:

```python
# Quick access to common styles
print_list.compact(items)    # Compact format, no count
print_list.wrapped(items)    # Wrapped format, no count  
print_list.columns(items)    # Column format, no count
print_list.numbered(items)   # Numbered format, no count
print_list.bullets(items)    # Bullet format, no count

# Specialized methods
print_list.as_table(data)    # Table with headers
print_list.summary(items)    # Summary for long lists
```

### Advanced Examples

```python
# Table with custom data
data = [['Name', 'Age', 'City'], ['Alice', 25, 'NYC'], ['Bob', 30, 'LA']]
print_list.as_table(data)

# Summary for long lists
long_list = list(range(100))
print_list.summary(long_list, max_items=6)  # Shows: [0, 1, 2, ..., 97, 98, 99]

# Custom print function (e.g., for logging)
def my_logger(msg):
    print(f"[LOG] {msg}")

print_list(items, style='bullet', print_func=my_logger)

# Combine partial with custom parameters
custom_compact = print_list(style='compact', sep=' | ')
custom_compact(items)  # Prints: apple | banana | cherry | date | elderberry | fig
```

### Key Features

- **Multiple Styles**: `wrapped`, `columns`, `numbered`, `bullet`, `compact`, `table`
- **Flexible Output**: Print directly or return strings with `print_func=None`
- **Partial Functions**: Create reusable printers with pre-configured settings
- **Customizable**: Control width, separators, line prefixes, and more
- **Type Safe**: Uses `Literal` types for style validation
- **Self-Contained**: No external dependencies beyond Python standard library

The `print_list` function is perfect for debugging, logging, user interfaces, and any situation where you need to display lists in a readable format.

<p class="epythet-aggregates">This documentation as a single file: <a href="lkj.md">lkj.md</a> (Markdown, for agents).</p>


# _autosummary/lkj.chunking.html.md

# lkj.chunking

Tools for chunking (segumentation, batching, slicing, etc.)

### Functions

| [`chunk_iterable`](_autosummary/lkj.chunking.html.md#lkj.chunking.chunk_iterable)(iterable, chk_size, \*[, ...])   | Divide an iterable into chunks/batches of a specific size.         |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`chunker`](_autosummary/lkj.chunking.html.md#lkj.chunking.chunker)(a, chk_size, \*[, include_tail])        | Chunks an iterable into non-overlapping chunks of size `chk_size`. |

### lkj.chunking.chunk_iterable(iterable, chk_size, , chunk_type=None)

Divide an iterable into chunks/batches of a specific size.

Handles both mappings (e.g. dicts) and non-mappings (lists, tuples, sets…)
as you probably expect it to (if you give a dict input, it will chunk on the
(key, value) items and return dicts of these).
Thought note that you always can control the type of the chunks with the
`chunk_type` argument.

* **Parameters:**
  * **iterable** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)] | [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]) – The iterable or mapping to divide.
  * **chk_size** ([`int`](https://docs.python.org/3/library/functions.html#int)) – The size of each chunk.
  * **chunk_type** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[`...`](https://docs.python.org/3/library/constants.html#Ellipsis), [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)] | [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]] | [`None`](https://docs.python.org/3/library/constants.html#None)) – The type of the chunks (list, tuple, set, dict…).
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`list`](https://docs.python.org/3/library/stdtypes.html#list)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)] | [`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`), [`...`](https://docs.python.org/3/library/constants.html#Ellipsis)] | [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]]
* **Returns:**
  An iterator of dicts if the input is a Mapping, otherwise an iterator
  of collections (list, tuple, set…).

### Examples

```pycon
>>> list(chunk_iterable([1, 2, 3, 4, 5], 2))
[[1, 2], [3, 4], [5]]
```

```pycon
>>> list(chunk_iterable((1, 2, 3, 4, 5), 3, chunk_type=tuple))
[(1, 2, 3), (4, 5)]
```

```pycon
>>> list(chunk_iterable({"a": 1, "b": 2, "c": 3}, 2))
[{'a': 1, 'b': 2}, {'c': 3}]
```

```pycon
>>> list(chunk_iterable({"x": 1, "y": 2, "z": 3}, 1, chunk_type=dict))
[{'x': 1}, {'y': 2}, {'z': 3}]
```

### lkj.chunking.chunker(a, chk_size, , include_tail=True)

Chunks an iterable into non-overlapping chunks of size `chk_size`.

#### NOTE
This chunker is simpler, but also less efficient than `chunk_iterable`.
It does have the extra `include_tail` argument, though.
Though note that you can get the effect of `include_tail=False` in `chunk_iterable`
by using `filter(lambda x: len(x) == chk_size, chunk_iterable(...))`.

* **Parameters:**
  * **a** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)]) – The iterable to be chunked.
  * **chk_size** ([`int`](https://docs.python.org/3/library/functions.html#int)) – The size of each chunk.
  * **include_tail** ([`bool`](https://docs.python.org/3/library/functions.html#bool)) – If True, includes the remaining elements as the last chunk
    even if they are fewer than `chk_size`. Defaults to True.
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`), [`...`](https://docs.python.org/3/library/constants.html#Ellipsis)]]
* **Returns:**
  An iterator of tuples, where each tuple is a chunk of size `chk_size`
  (or fewer elements if `include_tail` is True).

### Examples

```pycon
>>> list(chunker(range(8), 3))
[(0, 1, 2), (3, 4, 5), (6, 7)]
>>> list(chunker(range(8), 3, include_tail=False))
[(0, 1, 2), (3, 4, 5)]
```


# _autosummary/lkj.dicts.html.md

# lkj.dicts

Tools for working with dictionaries (and other Mappings).

If you are looking for more, check out the `lkj.iterables` module too
(after all, dicts are iterables).

### Functions

| [`compare_field_values`](_autosummary/lkj.dicts.html.md#lkj.dicts.compare_field_values)(dict1, dict2, \*[, ...])     | Compare two dictionaries' values field by field                                                                                     |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| [`exclusive_subdict`](_autosummary/lkj.dicts.html.md#lkj.dicts.exclusive_subdict)(d, exclude)                     | Returns a new dictionary with only the keys not in `exclude`.                                                                       |
| [`inclusive_subdict`](_autosummary/lkj.dicts.html.md#lkj.dicts.inclusive_subdict)(d, include)                     | Returns a new dictionary with only the keys in `include`.                                                                           |
| [`merge_dicts`](_autosummary/lkj.dicts.html.md#lkj.dicts.merge_dicts)(\*mappings[, ...])                    | Merge multiple mappings into a single mapping, recursively if needed, with customizable conflict resolution for non-mapping values. |
| [`truncate_dict_values`](_autosummary/lkj.dicts.html.md#lkj.dicts.truncate_dict_values)(d, \*[, max_list_size, ...]) | Returns a new dictionary with the same nested keys structure, where:                                                                |

### lkj.dicts.compare_field_values(dict1, dict2, \*, field_comparators={}, default_comparator=<built-in function eq>, aggregator=<function <lambda>>, get_comparison_fields=<function \_common_keys_list>)

Compare two dictionaries’ values field by field

* **Parameters:**
  * **dict1** – The first dictionary.
  * **dict2** – The second dictionary.
  * **field_comparators** ([`dict`](https://docs.python.org/3/library/stdtypes.html#dict)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`dict`](https://docs.python.org/3/library/stdtypes.html#dict), [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – A dictionary where keys are field names and values are comparator functions.
  * **default_comparator** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`dict`](https://docs.python.org/3/library/stdtypes.html#dict), [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – A default comparator function to use if no specific comparator is provided for a field.
  * **aggregator** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`dict`](https://docs.python.org/3/library/stdtypes.html#dict)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – A function to aggregate the comparison results into a final comparison object.
* **Returns:**
  A final score based on the comparison results.

```pycon
>>> dict1 = {"color": "brown", "animal": "dog"}
>>> dict2 = {"color": "brown", "animal": "cat"}
>>> dict3 = {"color": "brown", "animal": "bird"}
>>> field_comparators = {
...     "color": lambda x, y: 1 if x == y else 0,
...     "animal": lambda x, y: 1 if len(x) == len(y) else 0
... }
>>> compare_field_values(dict1, dict2, field_comparators=field_comparators)
{'color': 1, 'animal': 1}
>>> compare_field_values(dict1, dict3, field_comparators=field_comparators)
{'color': 1, 'animal': 0}
>>> import functools, statistics
>>> aggregator = lambda d: statistics.mean(d.values())
>>> mean_of_values = functools.partial(
...     compare_field_values, field_comparators=field_comparators, aggregator=aggregator
... )
>>> mean_of_values(dict1, dict2)
1
>>> mean_of_values(dict1, dict3)
0.5
```

### lkj.dicts.exclusive_subdict(d, exclude)

Returns a new dictionary with only the keys not in `exclude`.

### Parameters

d (dict): The input dictionary.
exclude (set): The set of keys to exclude from the new dictionary.

### Example

```pycon
>>> exclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'})
{'b': 2}
```

### lkj.dicts.inclusive_subdict(d, include)

Returns a new dictionary with only the keys in `include`.

### Parameters

d (dict): The input dictionary.
include (set): The set of keys to include in the new dictionary.

### Example

```pycon
>>> assert inclusive_subdict({'a': 1, 'b': 2, 'c': 3}, {'a', 'c'}) == {'a': 1, 'c': 3}
```

### lkj.dicts.merge_dicts(\*mappings, recursive_condition=<function <lambda>>, conflict_resolver=<function <lambda>>, mapping_constructor=<class 'dict'>)

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** ([`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]) – The mappings to merge.
  * **recursive_condition** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)], [`bool`](https://docs.python.org/3/library/functions.html#bool)]) – A callable to determine if values should be merged recursively.
    By default, checks if the value is a `Mapping`.
  * **conflict_resolver** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)], [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]) – A callable that resolves conflicts between two values.
    By default, overrides with the last seen value (`lambda x, y: y`).
  * **mapping_constructor** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]]], [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]]) – A callable to construct the resulting mapping.
    Defaults to the standard `dict` constructor.
* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]
* **Returns:**
  A merged mapping that combines all the input mappings.

### Examples

Basic usage with single-level merge (override behavior):

```pycon
>>> dict1 = {"a": 1}
>>> dict2 = {"a": 2, "b": 3}
>>> merge_dicts(dict1, dict2)
{'a': 2, 'b': 3}
```

Handling nested mappings with default behavior (override conflicts):

```pycon
>>> 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:

```pycon
>>> 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:

```pycon
>>> 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:

```pycon
>>> dict1 = {"a": {"nested": 1}}
>>> dict2 = {"a": {"nested": 2, "new": 3}}
>>> merge_dicts(dict1, dict2)
{'a': {'nested': 2, 'new': 3}}
```

```pycon
>>> 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`):

```pycon
>>> 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, \*, max_list_size=2, max_string_size=66, middle_marker='...', d_ingress=<function <lambda>>)

Returns a new dictionary with the same nested keys structure, where:

- List values are reduced to a maximum size of max_list_size.
- String values longer than max_string_size are truncated in the middle.
- Values are preprocessed with d_ingress before type checking.

### Parameters

d (dict): The input dictionary.
max_list_size (int, optional): Maximum size for lists. Defaults to 2.
max_string_size (int, optional): Maximum length for strings. Defaults to None (no truncation).
middle_marker (str, optional): String to insert in the middle of truncated strings. Defaults to ‘…’.
d_ingress (callable, optional): Function applied to each value before type checking.

> Defaults to identity function. Useful for handling special types like pandas Series.

### Returns

dict: A new dictionary with truncated lists and strings.

This can be useful when you have a large dictionary that you want to investigate,
but printing/logging it takes too much space.

### Example

```pycon
>>> large_dict = {'a': [1, 2, 3, 4, 5], 'b': {'c': [6, 7, 8, 9], 'd': 'A string like this that is too long'}, 'e': [10, 11]}
>>> truncate_dict_values(large_dict, max_list_size=3, max_string_size=20)
{'a': [1, 2, 3], 'b': {'c': [6, 7, 8], 'd': 'A string...too long'}, 'e': [10, 11]}
```

You can use `None` to indicate “no max”:

```pycon
>>> assert (
...     truncate_dict_values(large_dict, max_list_size=None, max_string_size=None)
...     == large_dict
... )
```

For handling special types like pandas Series:

```pycon
>>> def handle_pandas(obj):
...     if hasattr(obj, 'head'):  # pandas Series/DataFrame
...         return f"<{type(obj).__name__}: {len(obj)} items>"
...     return obj
>>> # truncate_dict_values(data_with_pandas, d_ingress=handle_pandas)
```

* **Return type:**
  [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)


# _autosummary/lkj.filesys.html.md

# lkj.filesys

File system utils

### Functions

| [`do_nothing`](_autosummary/lkj.filesys.html.md#lkj.filesys.do_nothing)(\*args, \*\*kwargs)                  | Function that does nothing.                                                               |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`enable_sourcing_from_file`](_autosummary/lkj.filesys.html.md#lkj.filesys.enable_sourcing_from_file)([func, write_output]) | Decorator for functions enables the decorated function to source from a file.             |
| [`get_app_data_dir`](_autosummary/lkj.filesys.html.md#lkj.filesys.get_app_data_dir)([dirname, if_exists, ...])     | Returns the full path of a directory suitable for storing application-specific data.      |
| [`get_watermarked_dir`](_autosummary/lkj.filesys.html.md#lkj.filesys.get_watermarked_dir)(dirname[, watermark, ...])  | Get a watermarked directory.                                                              |
| [`has_watermark`](_autosummary/lkj.filesys.html.md#lkj.filesys.has_watermark)(dirpath[, watermark])             | Check if a directory has a watermark.                                                     |
| [`rename_file`](_autosummary/lkj.filesys.html.md#lkj.filesys.rename_file)(file, renamer_function, \*[, ...])  | This function takes a list of files and renames them using the provided renamer function. |
| [`search_folder_fast`](_autosummary/lkj.filesys.html.md#lkj.filesys.search_folder_fast)(search_term[, ...])          | Executes a fast, recursive search using ripgrep and processes the results.                |
| `simple_jsonl_parser`(string)                                                                    |                                                                                           |
| [`watermark_dir`](_autosummary/lkj.filesys.html.md#lkj.filesys.watermark_dir)(dirpath[, watermark])             | Watermark.                                                                                |

### lkj.filesys.do_nothing(\*args, \*\*kwargs)

Function that does nothing.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

### lkj.filesys.enable_sourcing_from_file(func=None, , write_output=False)

Decorator for functions enables the decorated function to source from a file.

It is to be applied to functions that take a string or bytes as their first
argument. Decorating the function will enable it to detect if the first argument
is a file path, read the file content, call the function with the file content
as the first argument. Optionally, the decorated function can write the result
back to the file, or another file if specified.

* **Parameters:**
  **write_output** ([*bool*](https://docs.python.org/3/library/functions.html#bool) *or* [*str*](https://docs.python.org/3/library/stdtypes.html#str)) – If True, write the output back to the file. If a
  string, write the output to the specified file path. Default is False.

### lkj.filesys.get_app_data_dir(dirname='', \*, if_exists=<function do_nothing>, if_does_not_exist=<built-in function mkdir>, rootdir='/home/runner/.config/')

Returns the full path of a directory suitable for storing application-specific data.

It’s a mini-framework for creating a directories: It allows us to specify what to do
if the directory already exists, and what to do if it doesn’t exist.

Typical use case: We want to create a directory for storing application-specific
data, but we don’t want to write in a directory whose name is already taken if
it’s not “our” directory. To achieve this, we can specify `if_exists` to be a
function that verifies, through some condition on the content (example, watermark
or subdirectory structure), that the directory was indeed create by our
application, and 

```
``
```

if_does_not_exist\`\`to be a function that creates the directory
and populates it with a watermark or otherwise recognizable content.

* **Parameters:**
  * **dirname** – The name of the directory to create.
  * **if_exists** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – A function to call if the directory already exists.
    By default, it does nothing. The main non-default use case is to validate the
    contents of the directory, and/or populate it.
    If you write a custome `if_exists` function, it is your responsibility to
    return the full path of the directory (unless your use case doesn’t actually
    need that!)
  * **if_does_not_exist** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – A function to call if the directory does not exist.
    By default, it creates the directory with `os.mkdir`. If you need to also
    create subdirectories, you can use `os.makedirs`. You can also choose to
    raise an error, telling the user to create the directory manually.
  * **rootdir** ([`str`](https://docs.python.org/3/library/stdtypes.html#str))
* **Returns:**

If you specify nothing else, you’ll just get the system-dependent root directory for
storing application-specific data:

```pycon
>>> app_data_dir = get_app_data_dir()
>>> app_data_dir == APP_DATA_ROOTDIR
True
```

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.

```pycon
>>> 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(
...     'nonexistent_dir',
...     if_does_not_exist=notify_user_that_path_does_not_exist
... )
The 'nonexistent_dir' subdirectory doesn't exist
```

For an example of how to use this function as a framework to make custom directory
factories, see [`get_watermarked_dir()`](_autosummary/lkj.filesys.html.md#lkj.filesys.get_watermarked_dir).

### lkj.filesys.get_watermarked_dir(dirname, watermark='.lkj', \*, if_watermark_validation_fails=<function \_raise_watermark_error>, make_dir=<built-in function mkdir>, rootdir='/home/runner/.config/')

Get a watermarked directory.

```pycon
>>> 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
>>> # Make a
>>> f = partial(get_watermarked_dir, rootdir=testdir)
>>> mytestdir = f('mytestdir', '.my_watermark')
>>> os.listdir(testdir)
['mytestdir']
>>> os.listdir(mytestdir)
['.my_watermark']
>>> another_testdir = f('another_testdir')
>>> os.listdir(another_testdir)
['.lkj']
```

### lkj.filesys.has_watermark(dirpath, watermark='.lkj')

Check if a directory has a watermark.

### lkj.filesys.rename_file(file, renamer_function, , dry_run=True, verbose=True)

This function takes a list of files and renames them using the provided renamer function.

### lkj.filesys.search_folder_fast(search_term, path_to_search='.', \*, egress=<function \_ripgrep_json_parser>)

Executes a fast, recursive search using ripgrep and processes the results.

* **Parameters:**
  * **search_term** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The regex pattern or text string to search for.
  * **path_to_search** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The folder path to start searching from. Defaults to current directory.
  * **egress** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – A callable function to process the raw ripgrep output string.
    Defaults to a parser that returns a list of dictionaries for matches.
    If set to None, it defaults to a lambda returning the raw output (string).
    You can also give it simple_jsonl_parser to get a list of all JSON objects in the output.
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  The output of the ‘egress’ function.

## Example usage:

```pycon
>>> results = search_folder_fast("my_function_name", path_to_search='/path/to/project')
>>> for match in results:
...     print(f"Found in {match['path']} at line {match['line_number']}: {match['line_text']}")
```

### lkj.filesys.watermark_dir(dirpath, watermark='.lkj')

Watermark.


# _autosummary/lkj.funcs.html.md

# lkj.funcs

Tools for working with functions.

### Functions

| [`filter_greater_than`](_autosummary/lkj.funcs.html.md#lkj.funcs.filter_greater_than)(threshold, data)   | Filter elements in data that are greater than the threshold.                              |
|-----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`full_path_maker`](_autosummary/lkj.funcs.html.md#lkj.funcs.full_path_maker)(base_directory)        | Create a function that generates full file paths given filenames.                         |
| [`zip_with_filenames`](_autosummary/lkj.funcs.html.md#lkj.funcs.zip_with_filenames)([start_idx])        | Create a function that zips an iterable with file names like 'file_001', 'file_002', etc. |

### lkj.funcs.filter_greater_than(threshold, data)

Filter elements in data that are greater than the threshold.

### Example

```pycon
>>> filter_greater_than_5 = filter_greater_than(5, range(10))
>>> list(filter_greater_than_5)
[6, 7, 8, 9]
```

### lkj.funcs.full_path_maker(base_directory)

Create a function that generates full file paths given filenames.

### Example

```pycon
>>> full_path = full_path_maker('/base/directory')
>>> list(full_path(['file1', 'file2']))
['/base/directory/file1', '/base/directory/file2']
```

### lkj.funcs.zip_with_filenames(start_idx=1)

Create a function that zips an iterable with file names like ‘file_001’, ‘file_002’, etc.

### Example

```pycon
>>> zip_files = zip_with_filenames()
>>> dict(zip_files([1, 2]))
{'file_001': 1, 'file_002': 2}
>>> dict(zip_files(['never', 'say', 'never']))
{'file_004': 'never', 'file_005': 'say', 'file_006': 'never'}
```

Using count ensures that each label is unique and ordered sequentially.


# _autosummary/lkj.html.md

# lkj

Lightweight Kit Jumpstart

### Functions

| [`add_as_attribute_of`](_autosummary/lkj.html.md#lkj.add_as_attribute_of)(obj[, name])     | Decorator that adds a function as an attribute of a container object `obj`.   |
|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`add_attr`](_autosummary/lkj.html.md#lkj.add_attr)(attr_name[, attr_val, obj]) | Add an attribute to an object.                                                |
| `ddir`(obj)                                                                           |                                                                               |
| [`get_caller_package_name`](_autosummary/lkj.html.md#lkj.get_caller_package_name)([default])   | Return package name of caller                                                 |
| [`user_machine_id`](_autosummary/lkj.html.md#lkj.user_machine_id)()                    | Get an ID for the current computer/user that calls this function.             |

### lkj.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
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.

```pycon
>>> def foo():
...    pass
>>>
>>> @add_as_attribute_of(foo)
... def helper():
...    pass
>>> hasattr(foo, 'helper')
True
>>> callable(foo.helper)
True
```

In reality, any object that has a `__name__` can be added to the attribute of
`obj`, but the intention is to add helper functions to main “container” functions.

Note that if the name of the function starts with an underscore, it will be removed
before adding it as an attribute of `obj`.

```pycon
>>> @add_as_attribute_of(foo)
... def _helper():
...    pass
>>> hasattr(foo, 'helper')
True
```

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. But if you really want to add a function with a leading
underscore, you can do so by specifying the name explicitly:

```pycon
>>> @add_as_attribute_of(foo, name='_helper')
... def _helper():
...    pass
>>> hasattr(foo, '_helper')
True
```

Of course, you can give any name you want to the attribute:

```pycon
>>> @add_as_attribute_of(foo, name='bar')
... def _helper():
...    pass
>>> hasattr(foo, 'bar')
True
```

* **Parameters:**
  * **obj** – The object to which the function will be added as an attribute
  * **name** – The name of the attribute to add the function to. If not given, the

### lkj.add_attr(attr_name, attr_val=None, obj=None)

Add an attribute to an object.

If no object is provided, return a partial function that takes an object as its
argument.
If no attribute value is provided, return a partial function that takes an
attribute value as its argument.
If no object or attribute value is provided, return a partial function that takes
both an object and an attribute value as its arguments.
If all arguments are provided, add the attribute to the object and return the
object.

* **Parameters:**
  * **attr_name** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The name of the attribute to add.
  * **attr_val** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The value of the attribute to add.
  * **obj** – The object to which to add the attribute.
* **Returns:**
  The object with the attribute added, or a partial function that takes an
  object and/or an attribute value as its argument(s).

```pycon
>>> def generic_func(*args, **kwargs):
...     return args, kwargs
...
>>> generic_func.__name__
'generic_func'
>>>
>>> _ = add_attr('__name__', 'my_func', generic_func);
>>> generic_func.__name__
'my_func'
>>>
>>>
>>> add_name = add_attr('__name__')
>>> add_doc = add_attr('__doc__')
>>>
>>> @add_name('my_func')
... @add_doc('This is my function.')
... def f(*args, **kwargs):
...     return args, kwargs
...
>>> f.__name__
'my_func'
>>> f.__doc__
'This is my function.'
```

### lkj.get_caller_package_name(default=None)

Return package name of caller

See: [https://github.com/i2mint/i2mint/issues/1#issuecomment-1479416085](https://github.com/i2mint/i2mint/issues/1#issuecomment-1479416085)

### lkj.user_machine_id()

Get an ID for the current computer/user that calls this function.

### Modules

| [`chunking`](_autosummary/lkj.chunking.html.md#module-lkj.chunking)   | Tools for chunking (segumentation, batching, slicing, etc.)   |
|---------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`dicts`](_autosummary/lkj.dicts.html.md#module-lkj.dicts)         | Tools for working with dictionaries (and other Mappings).     |
| [`filesys`](_autosummary/lkj.filesys.html.md#module-lkj.filesys)     | File system utils                                             |
| [`funcs`](_autosummary/lkj.funcs.html.md#module-lkj.funcs)         | Tools for working with functions.                             |
| [`importing`](_autosummary/lkj.importing.html.md#module-lkj.importing) | Tools for importing                                           |
| [`iterables`](_autosummary/lkj.iterables.html.md#module-lkj.iterables) | Tools with iterables (dicts, lists, tuples, sets, etc.).      |
| [`loggers`](_autosummary/lkj.loggers.html.md#module-lkj.loggers)     | Utils for logging.                                            |
| [`misc`](_autosummary/lkj.misc.html.md#module-lkj.misc)           | Miscellaneous tools.                                          |
| [`strings`](_autosummary/lkj.strings.html.md#module-lkj.strings)     | String Utilities Module                                       |


# _autosummary/lkj.importing.html.md

# lkj.importing

Tools for importing

### Functions

| [`import_from_path`](_autosummary/lkj.importing.html.md#lkj.importing.import_from_path)([pkg_path, rootdir, ...])      | Import a package from a specified path.                            |
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| [`import_object`](_autosummary/lkj.importing.html.md#lkj.importing.import_object)(dot_path)                         | Imports and returns an object from a dot string path.              |
| [`parent_dir_of_module`](_autosummary/lkj.importing.html.md#lkj.importing.parent_dir_of_module)(module_obj, \*[, ...])     | Get the parent directory of a given module object.                 |
| [`register_namespace_forwarding`](_autosummary/lkj.importing.html.md#lkj.importing.register_namespace_forwarding)(source_base, ...) | Register the namespace forwarding from source_base to target_base. |

### Classes

| [`NamespaceForwardingFinder`](_autosummary/lkj.importing.html.md#lkj.importing.NamespaceForwardingFinder)(source_base, ...)   | A custom finder that detects when a module in the source namespace is being imported and forwards it to the target namespace.   |
|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| [`NamespaceForwardingLoader`](_autosummary/lkj.importing.html.md#lkj.importing.NamespaceForwardingLoader)(fullname, ...)      | A custom loader that forwards the import from a source namespace to a target namespace.                                         |

### *class* lkj.importing.NamespaceForwardingFinder(source_base, target_base)

Bases: [`MetaPathFinder`](https://docs.python.org/3/library/importlib.html#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.

#### source_base

The source namespace to detect and forward.

* **Type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

#### target_base

The target namespace to forward the import to.

* **Type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

### find_spec(fullname, path, target=None)::

Finds and returns the spec for the target module corresponding to the source module name.

### *class* lkj.importing.NamespaceForwardingLoader(fullname, source_base, target_base)

Bases: `Loader`

A custom loader that forwards the import from a source namespace to a target namespace.

#### fullname

The full name of the module being imported.

* **Type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

#### source_base

The base namespace to detect and forward from.

* **Type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

#### target_base

The base namespace to forward the import to.

* **Type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

### load_module(fullname)::

Loads and returns the target module corresponding to the source module name.

#### load_module(fullname)

Return the loaded module.

The module must be added to sys.modules and have import-related
attributes set properly.  The fullname is a str.

ImportError is raised on failure.

This method is deprecated in favor of loader.exec_module(). If
exec_module() exists then it is used to provide a backwards-compatible
functionality for this method.

### lkj.importing.import_from_path(pkg_path=None, , rootdir='', insert_in_globals=False)

Import a package from a specified path.

* **Parameters:**
  * **pkg_path** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The path to the package to import. If None, returns a partial function.
  * **rootdir** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The root directory from which to resolve the package path.
  * **insert_in_globals** ([`bool`](https://docs.python.org/3/library/functions.html#bool)) – If True, insert the imported package into the global namespace.
* **Returns:**
  The imported package or a partial function for deferred import.
* **Return type:**
  module or functools.partial
* **Raises:**
  [**ImportError**](https://docs.python.org/3/library/exceptions.html#ImportError) – If the package cannot be found or imported.

```pycon
>>> import os, inspect  # standard library modules (that are surely installed)
>>> rootdir = parent_dir_of_module(os)  # get the parent directory of os module
>>> os_module = import_from_path('os', rootdir=rootdir)
>>> os_module == os
True
```

```pycon
>>> import lkj  # a third-party module (that is surely installed)
>>> parent_dir_of_lkj = parent_dir_of_module(lkj, parent_levels=2)  # get the great-grandma directory of lkj module
```

Make a partial function to import lkj from its parent directory:

```pycon
>>> my_import_from_path = import_from_path(rootdir=parent_dir_of_lkj, insert_in_globals=True)
>>> lkj_module = my_import_from_path('lkj')
>>> assert 'lkj' in globals(), "Module 'lkj' should be imported into globals"
>>> lkj_module == lkj
True
```

### lkj.importing.import_object(dot_path)

Imports and returns an object from a dot string path.

```pycon
>>> f = import_object('os.path.join')
>>> from os.path import join
>>> f is join
True
```

### lkj.importing.parent_dir_of_module(module_obj, , parent_levels=0)

Get the parent directory of a given module object.

* **Parameters:**
  **module_obj** – The module object for which to find the parent directory.
* **Returns:**
  The parent directory of the module as a string.

```pycon
>>> import os  # a standard library module
>>> parent_dir_of_module(os)
'...python...'
>>> import lkj # a third-party module
>>> parent_dir_of_module(lkj)
'.../lkj'
```

### lkj.importing.register_namespace_forwarding(source_base, target_base)

Register the namespace forwarding from source_base to target_base.

* **Parameters:**
  * **source_base** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – The source namespace to forward.
  * **target_base** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – The target namespace to forward to.

Usage:

> If you put this code in the imbed.mdat package (say, containing a hcp module),

> ```pycon
> >>> register_namespace_forwarding('imbed.mdat', 'imbed_data_prep')
> ```

> Then when you do

> ```pycon
> >>> import imbed.mdat.hcp
> ```

> You’ll get the imbed_data_prep.hcp module.

This function inserts the custom finder into sys.meta_path, enabling the
dynamic import forwarding.


# _autosummary/lkj.iterables.html.md

# lkj.iterables

Tools with iterables (dicts, lists, tuples, sets, etc.).

### Functions

| [`compare_sets`](_autosummary/lkj.iterables.html.md#lkj.iterables.compare_sets)(left, right)                 | Compares two iterables and returns a named tuple with:         |
|--------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`get_by_value`](_autosummary/lkj.iterables.html.md#lkj.iterables.get_by_value)(list_of_dicts, value, field) | Get a dictionary from a list of dictionaries by a field value. |
| [`index_of`](_autosummary/lkj.iterables.html.md#lkj.iterables.index_of)(iterable, value)                 | List list.index but for any iterable.                          |

### Classes

| [`SetsComparisonResult`](_autosummary/lkj.iterables.html.md#lkj.iterables.SetsComparisonResult)(common, left_only, ...)   |    |
|-------------------------------------------------------------------------------------------------|----|

### *class* lkj.iterables.SetsComparisonResult(common, left_only, right_only)

Bases: [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)

#### common *: [set](https://docs.python.org/3/library/stdtypes.html#set)*

Alias for field number 0

#### left_only *: [set](https://docs.python.org/3/library/stdtypes.html#set)*

Alias for field number 1

#### right_only *: [set](https://docs.python.org/3/library/stdtypes.html#set)*

Alias for field number 2

### lkj.iterables.compare_sets(left, right)

Compares two iterables and returns a named tuple with:

- Elements in both iterables.
- Elements only in the left iterable.
- Elements only in the right iterable.

#### NOTE
When applied to dicts, the comparison is done on the keys.
If you want a comparison on the values, use `compare_iterables(left.values(), right.values())`.

* **Parameters:**
  * **left** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)) – The first iterable.
  * **right** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)) – The second iterable.
* **Returns:**
  A namedtuple with fields `common`,
  `left_only`, and `right_only`.
* **Return type:**
  [`SetsComparisonResult`](_autosummary/lkj.iterables.html.md#lkj.iterables.SetsComparisonResult)

### Examples

```pycon
>>> left = ['a', 'b', 'c']
>>> right = ['b', 'c', 'd']
>>> result = compare_sets(left, right)
>>> assert result.common == {'b', 'c'}  # asserting because order is not guaranteed
>>> result.left_only
{'a'}
>>> result.right_only
{'d'}
```

### lkj.iterables.get_by_value(list_of_dicts, value, field)

Get a dictionary from a list of dictionaries by a field value.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping)[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)]

```pycon
>>> data = [{'id': 1, 'value': 'A'}, {'id': 2, 'value': 'B'}]
>>> get_by_value(data, 2, 'id')
{'id': 2, 'value': 'B'}
```

This function just WANTS to be `functools.partial`-ized!!

```pycon
>>> from functools import partial
>>> get_by_id = partial(get_by_value, field='id')
>>> get_by_id(data, 1)
{'id': 1, 'value': 'A'}
>>> get_value_of_B = partial(get_by_value, value='B', field='value')
>>> get_value_of_B(data)
{'id': 2, 'value': 'B'}
```

### lkj.iterables.index_of(iterable, value)

List list.index but for any iterable.

* **Return type:**
  [`int`](https://docs.python.org/3/library/functions.html#int)

```pycon
>>> index_of(iter('abc'), 'b')
1
>>> index_of(iter(range(5)), 3)
3
>>> index_of(iter('abc'), 'z')
Traceback (most recent call last):
...
ValueError: 'z' is not in iterable
```


# _autosummary/lkj.loggers.html.md

# lkj.loggers

Utils for logging.

### Functions

| [`clog`](_autosummary/lkj.loggers.html.md#lkj.loggers.clog)(condition, \*args[, log_func])             | Conditional log                                                                                                             |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| `dflt_error_info_processor`(error_info, \*[, ...])                                               |                                                                                                                             |
| [`instance_flag_is_set`](_autosummary/lkj.loggers.html.md#lkj.loggers.instance_flag_is_set)(func, args, kwargs[, ...]) | Check if the log flag is set to True in the instance.                                                                       |
| [`log_calls`](_autosummary/lkj.loggers.html.md#lkj.loggers.log_calls)([func, logger, ingress_msg, ...])     | Decorator that adds logging before and after the function's call.                                                           |
| [`print_progress`](_autosummary/lkj.loggers.html.md#lkj.loggers.print_progress)(msg, \*[, refresh, ...])         | Prints with a timestamp and optional refresh.                                                                               |
| [`print_with_timestamp`](_autosummary/lkj.loggers.html.md#lkj.loggers.print_with_timestamp)(msg, \*[, refresh, ...])   | Prints with a timestamp and optional refresh.                                                                               |
| [`return_error_info_on_error`](_autosummary/lkj.loggers.html.md#lkj.loggers.return_error_info_on_error)(func, \*[, ...])     | Decorator that returns traceback and local variables on error.                                                              |
| [`wrap_text_with_exact_spacing`](_autosummary/lkj.loggers.html.md#lkj.loggers.wrap_text_with_exact_spacing)(text, \*[, ...])   | Prints a string with word-wrapping to a maximum line length, while preserving all existing newlines exactly as they appear. |
| [`wrapped_print`](_autosummary/lkj.loggers.html.md#lkj.loggers.wrapped_print)(items[, sep, max_width, ...])     | Prints a string or list ensuring the total line width does not exceed `max_width`.                                          |

### Classes

| [`CallOnError`](_autosummary/lkj.loggers.html.md#lkj.loggers.CallOnError)(\*exceptions[, on_error])     | An extension of the suppress context manager that enables the user to issue a warning message when an import error occurs.   |
|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| [`ErrorInfo`](_autosummary/lkj.loggers.html.md#lkj.loggers.ErrorInfo)(func, error, traceback, locals) |                                                                                                                              |

### *class* lkj.loggers.CallOnError(\*exceptions, on_error=<built-in function print>)

Bases: `suppress`

An extension of the suppress context manager that enables the user to issue a warning
message when an import error occurs.

```pycon
>>> warn_about_import_errors = CallOnError(ImportError, on_error=lambda err: print(f"Warning: {err}"))
>>> with warn_about_import_errors:
...     import this_package_surely_does_not_exist
Warning: No module named 'this_package_surely_does_not_exist'
>>> with warn_about_import_errors:
...     from os.this_module_does_not_exist import this_function_does_not_exist
Warning: No module named 'os.this_module_does_not_exist'; 'os' is not a package
```

### *class* lkj.loggers.ErrorInfo(func, error, traceback, locals)

Bases: [`object`](https://docs.python.org/3/library/functions.html#object)

### lkj.loggers.clog(condition, \*args, log_func=<built-in function print>, \*\*kwargs)

Conditional log

```pycon
>>> clog(False, "logging this")
>>> clog(True, "logging this")
logging this
```

One common usage is when there’s a verbose flag that allows the user to specify
whether they want to log or not. Instead of having to litter your code with
`if verbose:` statements you can just do this:

```pycon
>>> verbose = True  # say versbose is True
>>> _clog = clog(verbose)  # makes a clog with a fixed condition
>>> _clog("logging this")
logging this
```

You can also choose a different log function.
Usually you’d want to use a logger object from the logging module,
but for this example we’ll just use `print` with some modification:

```pycon
>>> _clog = clog(verbose, log_func=lambda x: print(f"hello {x}"))
>>> _clog("logging this")
hello logging this
```

### lkj.loggers.instance_flag_is_set(func, args, kwargs, flag_attr='verbose')

Check if the log flag is set to True in the instance.

### lkj.loggers.log_calls(func=None, \*, logger=<built-in function print>, ingress_msg=<function \_calling_name>, egress_msg=<function \_done_calling_name>, func_name=operator.attrgetter('_\_name_\_'), log_condition=<function \_always_log>)

Decorator that adds logging before and after the function’s call.

* **Parameters:**
  * **logger** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str)], [`None`](https://docs.python.org/3/library/constants.html#None)]) – The logger function to use. Default is print.
  * **ingress_msg** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str), [`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple), [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)], [`str`](https://docs.python.org/3/library/stdtypes.html#str)]) – The message to log before calling the function.
    If it returns None, no message is logged.
    Default is “Calling {name}…” where name is the name of the function.
  * **egress_msg** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/library/stdtypes.html#str), [`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple), [`dict`](https://docs.python.org/3/library/stdtypes.html#dict), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`str`](https://docs.python.org/3/library/stdtypes.html#str)]) – The message to log after the function call.
    If it returns None, no message is logged.
    Default is “…. Done”.
  * **func_name** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)], [`str`](https://docs.python.org/3/library/stdtypes.html#str)]) – The function to use to get the function’s name.
  * **log_condition** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable), [`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple), [`dict`](https://docs.python.org/3/library/stdtypes.html#dict)], [`bool`](https://docs.python.org/3/library/functions.html#bool)]) – The condition for logging.
    Should be a callable that takes a function, args, and kwargs,
    and return a bool. If log_condition returns False, no logging is done.
    Default is \_always_log.
* **Return type:**
  [*Callable*](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)

Tips:

```default
Use `logger=print_with_timestamp` to get timestamps in your logs.
Use `lambda *args: None` as the ingress_msg or egress_msg to suppress logging.
Use `logger=lambda x: None` to suppress all logging.
```

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)
* **Returns:**
  The decorated function.

### Example

```pycon
>>> @log_calls
... def add(a, b):
...     return a + b
...
>>> add(2, 3)
Calling add...
.... Done calling add
5
```

```pycon
>>> @log_calls(
...     logger=lambda x: print(f"LOG: {x}"),
...     ingress_msg=lambda name, *args: f"Start {name}!",
...     egress_msg=lambda *args: "End"
... )
... def multiply(a, b):
...     return a * b
...
>>> multiply(2, 3)
LOG: Start multiply!
LOG: End
6
```

Sometimes, you want to dynamically control whether to log or not.
This is what the `log_condition` parameter is for.
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:

```pycon
>>> import functools
>>> log_if_verbose_set_to_true = functools.partial(
...     log_calls.instance_flag_is_set, flag_attr='verbose'
... )
```

Now if you have a class with a `verbose` attribute, you can use this helper function
to log only if `verbose` is set:

```pycon
>>> class MyClass:
...     def __init__(self, verbose=False):
...         self.verbose = verbose
...
...     @log_calls(log_condition=log_if_verbose_set_to_true)
...     def foo(self):
...         print("Executing foo")
...
>>> # Example usage
>>> obj = MyClass(verbose=True)
>>> obj.foo()  # This will log
Calling foo...
Executing foo
.... Done calling foo
```

But if verbose is set to `False`, no logging will be done:

```pycon
>>> obj = MyClass(verbose=False)
>>> obj.foo()  # This will not log
Executing foo
```

### lkj.loggers.print_progress(msg, \*, refresh=None, display_time=True, print_func=<built-in function print>)

Prints with a timestamp and optional refresh.

input: message, and possibly args (to be placed in the message string, sprintf-style

output: Displays the time (HH:MM:SS), and the message

use: To be able to track processes (and the time they take)

### lkj.loggers.print_with_timestamp(msg, \*, refresh=None, display_time=True, print_func=<built-in function print>)

Prints with a timestamp and optional refresh.

input: message, and possibly args (to be placed in the message string, sprintf-style

output: Displays the time (HH:MM:SS), and the message

use: To be able to track processes (and the time they take)

### lkj.loggers.return_error_info_on_error(func, \*, caught_error_types=(<class 'Exception'>, ), error_info_processor=<function dflt_error_info_processor>)

Decorator that returns traceback and local variables on error.

This decorator is useful for debugging. It will catch any exceptions that occur
in the decorated function, and return an ErrorInfo object with the traceback and
local variables at the time of the error.

* **Parameters:**
  * **func** – The function to decorate.
  * **caught_error_types** ([`tuple`](https://docs.python.org/3/library/stdtypes.html#tuple)[[`Exception`](https://docs.python.org/3/library/exceptions.html#Exception)]) – The types of errors to catch.
  * **error_info_processor** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`ErrorInfo`](_autosummary/lkj.loggers.html.md#lkj.loggers.ErrorInfo)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – A function that processes the ErrorInfo object.

#### TIP
To parametrize this decorator, you can use a functools.partial function.

#### TIP
You can have your error_info_processor persist the error info to a file or
database, or send it to a logging service.

```pycon
>>> @return_error_info_on_error
... def foo(x, y=2):
...     return x / y
...
>>> t = foo(1, 2)
>>> assert t == 0.5
>>> t = foo(1, y=0)
Exiting from foo with error: division by zero
>>> if isinstance(t, ErrorInfo):
...     assert isinstance(t.error, ZeroDivisionError)
...     hasattr(t, 'traceback')
...     assert t.locals['args'] == (1,)
...     assert t.locals['kwargs'] == {'y': 0}
```

### lkj.loggers.wrap_text_with_exact_spacing(text, \*, max_width=80, print_func=<built-in function print>, line_prefix='')

Prints a string with word-wrapping to a maximum line length, while preserving all existing newlines
exactly as they appear.

### Args

- text (str): The text to wrap and print.
- max_width (int): The maximum width of each line (default is 88).

### lkj.loggers.wrapped_print(items, sep=', ', max_width=80, \*, print_func=<built-in function print>, line_prefix='')

Prints a string or list ensuring the total line width does not exceed `max_width`.

If adding a new item would exceed this width, it starts a new line.

* **Parameters:**
  * **items** ([`str`](https://docs.python.org/3/library/stdtypes.html#str) | [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)) – String or list of items to print.
  * **sep** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – The separator to use between items.
  * **max_width** ([*int*](https://docs.python.org/3/library/functions.html#int)) – The maximum width of each line. Default is 80.

### Example

```pycon
>>> items = [
...     "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8",
...     "item9", "item10"
... ]
>>> sep = ", "
>>> wrapped_print(items, sep, max_width=30)
item1, item2, item3, item4,
item5, item6, item7, item8,
item9, item10
```

```pycon
>>> items = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
>>> sep = " - "
>>> wrapped_print(items, sep, max_width=10)
a - b - c
- d - e -
f - g - h
- i - j
```

Note that you have control over the `print_func`.
This, for example, allows you to just return the string instead of printing it.

```pycon
>>> wrapped_print(items, sep, max_width=10, print_func=lambda x: x)
'a - b - c\n- d - e -\nf - g - h\n- i - j'
```


# _autosummary/lkj.misc.html.md

# lkj.misc

Miscellaneous tools.

### Functions

| `identity`(x)                                                                                  |    |
|------------------------------------------------------------------------------------------------|----|
| [`value_in_interval`](_autosummary/lkj.misc.html.md#lkj.misc.value_in_interval)([x, get_val, min_val, ...]) |    |

### lkj.misc.value_in_interval(x=None, /, \*, get_val=<function identity>, min_val=None, max_val=None, is_minimum=<built-in function ge>, is_maximum=<built-in function lt>)

```pycon
>>> 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))
[False, True, True, False, False]
```

The default `is_maximum` is `lt` (i.e. lambda x: x < max_val). If you want to
use `le` (i.e. lambda x: x <= max_val) you can change the is_maximum argument:

```pycon
>>> ff = value_in_interval(
...     get_val=itemgetter('date'), min_val=2, max_val=8, is_maximum=le
... )
>>> list(map(ff, d))
[False, True, True, True, False]
```


# _autosummary/lkj.strings.html.md

# lkj.strings

String Utilities Module

This module provides a comprehensive set of utility functions and classes for working with strings in Python.
It includes tools for string manipulation, formatting, pretty-printing, and find/replace operations.

Core Components:

- StringAppender: A helper class for collecting strings, useful for capturing output that would otherwise be printed.
- indent_lines: Indents each line of a string by a specified prefix.
- most_common_indent: Determines the most common indentation used in a multi-line string.
- FindReplaceTool: A class for advanced find-and-replace operations on strings, supporting regular expressions, match history, and undo functionality.

Pretty-Printing Functions:

- print_list: Prints lists in various human-friendly formats (wrapped, columns, numbered, bullet, table, compact), with options for width, separators, and custom print functions.
- print_list.as_table: Formats and prints a list (or list of lists) as a table, with optional headers and alignment.
- print_list.summary: Prints a summary of a list, showing first few and last few items if the list is long.
- print_list.compact, print_list.wrapped, print_list.columns, print_list.numbered, print_list.bullets: Convenience methods using print_list’s partial functionality for common display styles.

These utilities are designed to make it easier to display, format, and manipulate strings and collections of strings in a readable and flexible way.

### Functions

| [`camel_to_snake`](_autosummary/lkj.strings.html.md#lkj.strings.camel_to_snake)(camel_string)                      | Convert a CamelCase string to snake_case.                                                           |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `fields_of_string_format`(template)                                                                |                                                                                                     |
| [`fields_of_string_formats`](_autosummary/lkj.strings.html.md#lkj.strings.fields_of_string_formats)(templates, \*[, ...])    | Extract all unique field names from the templates in \_github_url_templates using string.Formatter. |
| `identity`(x)                                                                                      |                                                                                                     |
| [`indent_lines`](_autosummary/lkj.strings.html.md#lkj.strings.indent_lines)(string, indent, \*[, line_sep])      | Indent each line of a string.                                                                       |
| [`most_common_indent`](_autosummary/lkj.strings.html.md#lkj.strings.most_common_indent)(string[, ignore_first_line])   | Find the most common indentation in a string.                                                       |
| [`print_list`](_autosummary/lkj.strings.html.md#lkj.strings.print_list)([items, style, max_width, sep, ...])   | Print a list in a nice, readable format with multiple style options.                                |
| [`print_list_as_table`](_autosummary/lkj.strings.html.md#lkj.strings.print_list_as_table)(items[, headers, ...])        | Print a list as a nicely formatted table.                                                           |
| [`print_list_summary`](_autosummary/lkj.strings.html.md#lkj.strings.print_list_summary)(items, \*[, max_items, ...])   | Print a summary of a list, showing first few and last few items if the list is long.                |
| [`regex_based_substitution`](_autosummary/lkj.strings.html.md#lkj.strings.regex_based_substitution)(replacements[, ...])     | Construct a substitution function based on an iterable of replacement pairs.                        |
| [`snake_to_camel`](_autosummary/lkj.strings.html.md#lkj.strings.snake_to_camel)(snake_string)                      | Convert a snake_case string to CamelCase.                                                           |
| [`truncate_lines`](_autosummary/lkj.strings.html.md#lkj.strings.truncate_lines)(s[, top_limit, bottom_limit, ...]) | Truncates a string by limiting the number of lines from the top and bottom.                         |
| [`truncate_string`](_autosummary/lkj.strings.html.md#lkj.strings.truncate_string)(s, \*[, left_limit, ...])         | Truncate a string to a maximum length, inserting a marker in the middle.                            |
| [`truncate_string_with_marker`](_autosummary/lkj.strings.html.md#lkj.strings.truncate_string_with_marker)(s, \*[, ...])         | Truncate a string to a maximum length, inserting a marker in the middle.                            |
| [`unique_affixes`](_autosummary/lkj.strings.html.md#lkj.strings.unique_affixes)(items[, suffix, egress, ingress])  | Returns a list of unique prefixes (or suffixes) for the given iterable of sequences.                |

### Classes

| [`FindReplaceTool`](_autosummary/lkj.strings.html.md#lkj.strings.FindReplaceTool)(text, \*[, line_mode, flags, ...])   | A general-purpose find-and-replace tool that can treat the input text as a continuous sequence of characters, even if operations such as viewing context are performed line by line.   |
|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`StringAppender`](_autosummary/lkj.strings.html.md#lkj.strings.StringAppender)([separator])                          | Helper class to collect strings instead of printing them directly.                                                                                                                     |
| `TrieNode`()                                                                                          |                                                                                                                                                                                        |

### *class* lkj.strings.FindReplaceTool(text, , line_mode=False, flags=0, show_line_numbers=True, context_size=2, highlight_char='^')

Bases: [`object`](https://docs.python.org/3/library/functions.html#object)

A general-purpose find-and-replace tool that can treat the input text
as a continuous sequence of characters, even if operations such as viewing
context are performed line by line. The tool can analyze matches based on
a user-supplied regular expression, navigate through the matches with context,
and perform replacements either interactively or in bulk. Replacements can be
provided as either a static string or via a callback function that receives details
of the match.

Instead of keeping a single modified text, this version maintains a history of
text versions in self._text_versions, where self._text_versions[0] is the original
text and self._text_versions[-1] is the current text. Each edit is performed on the
current version and appended to the history. Additional methods allow reverting changes.

## 1: Basic usage

```pycon
>>> FindReplaceTool("apple banana apple").find_and_print_matches(r'apple')
Match 0 (around line 1):
apple banana apple
^^^^^
----------------------------------------
Match 1 (around line 1):
apple banana apple
             ^^^^^
----------------------------------------
>>> FindReplaceTool("apple banana apple").find_and_replace(r'apple', "orange")
'orange banana orange'
```

## 2: Using line_mode=True with a static replacement.

```pycon
>>> text1 = "apple\nbanana apple\ncherry"
>>> tool = FindReplaceTool(text1, line_mode=True, flags=re.MULTILINE)
>>> import re
>>> # Find all occurrences of "apple" (two in total).
>>> _ = tool.analyze(r'apple')
>>> len(tool._matches)
2
>>> # Replace the first occurrence ("apple" on the first line) with "orange".
>>> tool.replace_one(0, "orange").get_modified_text()
'orange\nbanana apple\ncherry'
```

## 3: Using line_mode=False with a callback replacement.

```pycon
>>> text2 = "apple banana apple"
>>> tool2 = FindReplaceTool(text2, line_mode=False)
>>> # Find all occurrences of "apple" in the continuous text.
>>> len(tool2.analyze(r'apple')._matches)
2
>>> # Define a callback that converts each matched text to uppercase.
>>> def to_upper(match):
...     return match["matched_text"].upper()
>>> tool2.replace_all(to_upper).get_modified_text()
'APPLE banana APPLE'
```

## 4: Reverting changes.

```pycon
>>> text3 = "one two three"
>>> tool3 = FindReplaceTool(text3)
>>> import re
>>> # Analyze to match the first word "one" (at the start of the text).
>>> tool3.analyze(r'^one').replace_one(0, "ONE").get_modified_text()
'ONE two three'
>>> # Revert the edit.
>>> tool3.revert()
'one two three'
```

#### analyze(pattern)

Searches the current text (the last version) for occurrences matching the given
regular expression. Any match data (including group captures) is stored internally.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

#### find_and_print_matches(pattern)

Searches the current text (the last version) for occurrences matching the given
regular expression. Any match data (including group captures) is stored internally.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

#### find_and_replace(pattern, replacement)

Searches the current text (the last version) for occurrences matching the given
regular expression. Any match data (including group captures) is stored internally.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

#### get_modified_text()

Returns the current (latest) text version.

* **Return type:**
  [`str`](https://docs.python.org/3/library/stdtypes.html#str)

#### get_original_text()

Returns the original text (first version).

* **Return type:**
  [`str`](https://docs.python.org/3/library/stdtypes.html#str)

#### replace_all(replacement)

Replaces all stored matches in the current text version. The ‘replacement’ argument may
be a static string or a callable (see replace_one for details). Replacements are performed
from the last match to the first, so that earlier offsets are not affected.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

#### replace_one(match_index, replacement)

Replaces a single match, identified by match_index, with a new string.
The ‘replacement’ argument may be either a static string or a callable.
When it is a callable, it is called with a dictionary containing the match data
(including any captured groups) and should return the replacement string.
The replacement is performed on the current text version, and the new text is
appended as a new version in the history.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

#### revert(steps=1)

Reverts the current text version by removing the last ‘steps’ versions
from the history. The original text (version 0) is never removed.
Returns the new current text.

```pycon
>>> text = "one two three"
>>> tool = FindReplaceTool(text)
>>> import re
>>> tool.analyze(r'^one').replace_one(0, "ONE").get_modified_text()
'ONE two three'
>>> tool.revert()
'one two three'
```

#### view_matches()

Displays all stored matches along with surrounding context. When line_mode
is enabled, the context is provided in full lines with (optionally) line numbers,
and a line is added below the matched line to indicate the matched portion.
In non-line mode, a snippet of characters around the match is shown.

* **Return type:**
  [`None`](https://docs.python.org/3/library/constants.html#None)

### *class* lkj.strings.StringAppender(separator='\\n')

Bases: [`object`](https://docs.python.org/3/library/functions.html#object)

Helper class to collect strings instead of printing them directly.

#### get_string()

Alternative way to get the string.

### lkj.strings.camel_to_snake(camel_string)

Convert a CamelCase string to snake_case. Useful for converting class
names to variable names.

* **Parameters:**
  **camel_string** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – The CamelCase string to convert.
* **Returns:**
  The converted snake_case string.
* **Return type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

### Examples

```pycon
>>> camel_to_snake('BasicParseTest')
'basic_parse_test'
>>> camel_to_snake('HTMLParser')
'html_parser'
>>> camel_to_snake('CamelCaseExample')
'camel_case_example'
```

Note that acronyms are handled correctly:

```pycon
>>> camel_to_snake('XMLHttpRequestTest')
'xml_http_request_test'
```

### lkj.strings.fields_of_string_formats(templates, \*, aggregator=<class 'set'>)

Extract all unique field names from the templates in \_github_url_templates using string.Formatter.

* **Parameters:**
  **templates** ([*list*](https://docs.python.org/3/library/stdtypes.html#list)) – A list of dictionaries containing ‘template’ keys.
* **Returns:**
  A sorted list of unique field names found in the templates.
* **Return type:**
  [*list*](https://docs.python.org/3/library/stdtypes.html#list)

### Example

```pycon
>>> templates = ['{this}/and/{that}', 'and/{that}/is/an/{other}']
>>> sorted(fields_of_string_formats(templates))
['other', 'that', 'this']
```

### lkj.strings.indent_lines(string, indent, , line_sep='\\\\n')

Indent each line of a string.

* **Parameters:**
  * **string** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The string to indent.
  * **indent** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The string to use for indentation.
* **Return type:**
  [`str`](https://docs.python.org/3/library/stdtypes.html#str)
* **Returns:**
  The indented string.

```pycon
>>> print(indent_lines('This is a test.\nAnother line.', ' ' * 8))
        This is a test.
        Another line.
```

### lkj.strings.most_common_indent(string, ignore_first_line=False)

Find the most common indentation in a string.

* **Parameters:**
  * **string** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – The string to analyze.
  * **ignore_first_line** – Whether to ignore the first line when determining the
    indentation. Default is False. One case where you want True is when using python
    triple quotes (as in docstrings, for example), since the first line often has
    no indentation (from the point of view of the string, in this case.
* **Return type:**
  [`str`](https://docs.python.org/3/library/stdtypes.html#str)
* **Returns:**
  The most common indentation string.

### Examples

```pycon
>>> most_common_indent('    This is a test.\n    Another line.')
'    '
```

### lkj.strings.print_list(items=None, \*, style='wrapped', max_width=80, sep=', ', line_prefix='', items_per_line=None, show_count=False, title=None, print_func=<built-in function print>)

Print a list in a nice, readable format with multiple style options.

* **Parameters:**
  * **items** ([`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/library/constants.html#None)) – The list or iterable to print. If None, returns a partial function.
  * **style** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'wrapped'`, `'columns'`, `'numbered'`, `'bullet'`, `'table'`, `'compact'`]) – One of “wrapped”, “columns”, “numbered”, “bullet”, “table”, “compact”
  * **max_width** ([`int`](https://docs.python.org/3/library/functions.html#int)) – Maximum width for wrapped style
  * **sep** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – Separator for items
  * **line_prefix** ([`str`](https://docs.python.org/3/library/stdtypes.html#str)) – Prefix for each line
  * **items_per_line** – For columns style, how many items per line
  * **show_count** ([`bool`](https://docs.python.org/3/library/functions.html#bool) | [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`int`](https://docs.python.org/3/library/functions.html#int)], [`str`](https://docs.python.org/3/library/stdtypes.html#str)]) – Whether to prefix with the count of items
  * **title** – Optional title to display before the list
  * **print_func** – Function to use for printing. Defaults to print.
    If None, returns the string instead of printing.

### Examples

```pycon
>>> items = ["apple", "banana", "cherry", "date", "elderberry", "fig"]
```

### Wrapped style (default)

```pycon
>>> print_list(items, max_width=30)
apple, banana, cherry, date,
elderberry, fig
```

### Columns style

```pycon
>>> print_list(items, style="columns", items_per_line=3)
apple banana     cherry
date  elderberry fig
```

### Numbered style

```pycon
>>> print_list(items, style="numbered")
1. apple
2. banana
3. cherry
4. date
5. elderberry
6. fig
```

### Bullet style

```pycon
>>> print_list(items, style="bullet")
• apple
• banana
• cherry
• date
• elderberry
• fig
```

### Return string instead of printing

```pycon
>>> result = print_list(items, style="numbered", print_func=None, show_count=True)
>>> print(result)
List (6 items):
1. apple
2. banana
3. cherry
4. date
5. elderberry
6. fig
```

Partial function functionality: If you don’t specify the items (or items=None),
the function returns a partial function that can be called with the items later.
That is, the print_list acts as a factory function for different
printing styles.

```pycon
>>> numbered_printer = print_list(style="numbered", show_count=False)
>>> numbered_printer(items)
1. apple
2. banana
3. cherry
4. date
5. elderberry
6. fig
```

```pycon
>>> compact_printer = print_list(style="compact", max_width=60, show_count=False)
>>> compact_printer(items)
apple, banana, cherry, date, elderberry, fig
```

```pycon
>>> bullet_printer = print_list(style="bullet", print_func=None, show_count=False)
>>> result = bullet_printer(items)
>>> print(result)
• apple
• banana
• cherry
• date
• elderberry
• fig
```

### lkj.strings.print_list_as_table(items, headers=None, \*, max_width=80, align='left', print_func=<built-in function print>)

Print a list as a nicely formatted table.

* **Parameters:**
  * **items** – List of items (strings, numbers, or objects with \_\_str_\_)
  * **headers** – Optional list of column headers
  * **max_width** – Maximum width of the table
  * **align** – Alignment for columns (“left”, “right”, “center”)
  * **print_func** – Function to use for printing. Defaults to print.
    If None, returns the string instead of printing.

### Examples

```pycon
>>> data = [["Name", "Age", "City"], ["Alice", 25, "NYC"], ["Bob", 30, "LA"]]
>>> print_list_as_table(data)
Name  | Age | City
-----|---|----
Alice | 25  | NYC
Bob   | 30  | LA
```

### Return string instead of printing

```pycon
>>> result = print_list_as_table(data, print_func=None)
>>> print(result)
Name  | Age | City
-----|---|----
Alice | 25  | NYC
Bob   | 30  | LA
```

### lkj.strings.print_list_summary(items, \*, max_items=10, show_total=True, title=None, print_func=<built-in function print>)

Print a summary of a list, showing first few and last few items if the list is long.

* **Parameters:**
  * **items** – The list to summarize
  * **max_items** – Maximum number of items to show (first + last)
  * **show_total** – Whether to show the total count
  * **title** – Optional title
  * **print_func** – Function to use for printing. Defaults to print.
    If None, returns the string instead of printing.

### Examples

```pycon
>>> long_list = list(range(100))
>>> print_list_summary(long_list, max_items=6)
List (100 items):
[0, 1, 2, ..., 97, 98, 99]
```

```pycon
>>> print_list_summary(long_list, max_items=10)
List (100 items):
[0, 1, 2, 3, 4, ..., 95, 96, 97, 98, 99]
```

### Return string instead of printing

```pycon
>>> result = print_list_summary(long_list, max_items=6, print_func=None)
>>> print(result)
List (100 items):
[0, 1, 2, ..., 97, 98, 99]
```

### lkj.strings.regex_based_substitution(replacements, regex=None, s=None)

Construct a substitution function based on an iterable of replacement pairs.

* **Parameters:**
  **replacements** ([`dict`](https://docs.python.org/3/library/stdtypes.html#dict)) – An iterable of (replace_this, with_that) pairs.
* **Returns:**
  A function that, when called with a string, will perform all substitutions.
* **Return type:**
  *Callable*[[[*str*](https://docs.python.org/3/library/stdtypes.html#str)], [*str*](https://docs.python.org/3/library/stdtypes.html#str)]

The function is meant to be used with `replacements` as its single input,
returning a `substitute` function that will carry out the substitutions
on an input string.

```pycon
>>> replacements = {'apple': 'orange', 'banana': 'grape'}
>>> substitute = regex_based_substitution(replacements)
>>> substitute("I like apple and bananas.")
'I like orange and grapes.'
```

You have access to the `replacements` and `regex` attributes of the
`substitute` function. See how the replacements dict has been ordered by
descending length of keys. This is to ensure that longer keys are replaced
before shorter keys, avoiding partial replacements.

```pycon
>>> substitute.replacements
{'banana': 'grape', 'apple': 'orange'}
```

### lkj.strings.snake_to_camel(snake_string)

Convert a snake_case string to CamelCase. Useful for converting variable
names to class names.

* **Parameters:**
  **snake_string** ([*str*](https://docs.python.org/3/library/stdtypes.html#str)) – The snake_case string to convert.
* **Returns:**
  The converted CamelCase string.
* **Return type:**
  [*str*](https://docs.python.org/3/library/stdtypes.html#str)

### Examples

```pycon
>>> snake_to_camel('complex_tokenizer')
'ComplexTokenizer'
>>> snake_to_camel('simple_example_test')
'SimpleExampleTest'
```

Note that acronyms are capitalized correctly:

```pycon
>>> snake_to_camel('xml_http_request_test')
'XmlHttpRequestTest'
```

### lkj.strings.truncate_lines(s, top_limit=None, bottom_limit=None, middle_marker='...')

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.

* **Return type:**
  [`str`](https://docs.python.org/3/library/stdtypes.html#str)

### Example

```pycon
>>> text = '''Line1
... Line2
... Line3
... Line4
... Line5
... Line6'''
```

```pycon
>>> print(truncate_lines(text, top_limit=2, bottom_limit=2))
Line1
Line2
...
Line5
Line6
```

### lkj.strings.truncate_string(s, , 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.

```pycon
>>> truncate_string('1234567890')
'1234567890'
```

But if the string is longer than the sum of the limits, it is truncated:

```pycon
>>> 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:

```pycon
>>> 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='...')

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.

```pycon
>>> truncate_string('1234567890')
'1234567890'
```

But if the string is longer than the sum of the limits, it is truncated:

```pycon
>>> 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:

```pycon
>>> 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.unique_affixes(items, suffix=False, \*, egress=None, ingress=<function identity>)

Returns a list of unique prefixes (or suffixes) for the given iterable of sequences.
Raises a ValueError if duplicates are found.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence)]

### Parameters

- items: Iterable of sequences (e.g., list of strings).
- suffix: If True, finds unique suffixes instead of prefixes.
- ingress: Callable to preprocess each item. Default is identity function.
- egress: Callable to postprocess each affix. Default is appropriate function based on item type.
  Usually, ingress and egress are inverses of each other.

```pycon
>>> unique_affixes(['apple', 'ape', 'apricot', 'banana', 'band', 'bandana'])
['app', 'ape', 'apr', 'bana', 'band', 'banda']
```

```pycon
>>> unique_affixes(['test', 'testing', 'tester'])
['test', 'testi', 'teste']
```

```pycon
>>> unique_affixes(['test', 'test'])
Traceback (most recent call last):
...
ValueError: Duplicate item detected: test
```

```pycon
>>> unique_affixes(['abc', 'abcd', 'abcde'])
['abc', 'abcd', 'abcde']
```

```pycon
>>> unique_affixes(['a', 'b', 'c'])
['a', 'b', 'c']
```

```pycon
>>> unique_affixes(['x', 'xy', 'xyz'])
['x', 'xy', 'xyz']
```

```pycon
>>> unique_affixes(['can', 'candy', 'candle'])
['can', 'candy', 'candl']
```

```pycon
>>> unique_affixes(['flow', 'flower', 'flight'])
['flow', 'flowe', 'fli']
```

```pycon
>>> unique_affixes(['ation', 'termination', 'examination'], suffix=True)
['ation', 'rmination', 'amination']
```

```pycon
>>> import functools
>>> ingress = functools.partial(str.split, sep='.')
>>> egress = '.'.join
>>> items = ['here.and.there', 'here.or.there', 'here']
>>> unique_affixes(items, ingress=ingress, egress=egress)
['here.and', 'here.or', 'here']
```


# api.html.md

# API reference

| [`lkj`](_autosummary/lkj.html.md#module-lkj)   | Lightweight Kit Jumpstart   |
|-------------------------------------------------------------------|-----------------------------|


