Skip to content

Repository files navigation

i2

Core tools for minting code.

For human readers: Documentation here.

For AI agents, and humans that use them: this repo ships agent skills — focused, task-oriented guides that tell a coding agent when and how to reach for i2's tools:

  • i2-signatures — introspect signatures and bind a call's *args, **kwargs to named arguments.
  • i2-sig-arithmetic — build, merge (+/-) and edit function signatures.
  • i2-wrapper — wrap functions to transform their interface, inputs and output.
  • i2-castgraph — route data through a graph of type/representation conversions.
  • i2-multi-object — compose a fixed set of functions or context managers (Pipe, FuncFanout, …).

Key Modules Overview

i2.castgraph - Type/Kind-Based Transformation Graphs

castgraph provides a graph-based system for organizing transformations between different data representations ("kinds"). It routes objects through multi-hop conversion paths, selecting the optimal route based on cost.

The castgraph tool addresses the common friction point in software design where a function requires a specific data type or format, but the user possesses related data in a different, interchangeable representation (e.g., a file path instead of a loaded object). Inspired by Postel's Law ("be liberal in what you accept"), the core problem is how to make interfaces highly flexible and accommodating of diverse inputs—eliminating tedious data preparation boilerplate for the user—while simultaneously adhering to the principle that "explicit is better than implicit" by keeping complex conversion logic out of the main application code; i2.castgraph solves this by providing a dedicated, cost-aware graph system to organize and execute necessary multi-hop transformations dynamically, effectively acting as an intelligent input adapter. Read me in the castgraph dev notes.

Basic Usage (Type-Based):

fromi2.castgraphimportTransformationGraphgraph=TransformationGraph()
# Register transformations between types@graph.register_edge(str, float)defstr_to_float(s, ctx):
returnfloat(s)
@graph.register_edge(float, int)deffloat_to_int(f, ctx):
returnint(f)
# Automatically routes str -> float -> intresult=graph.transform("42.7", int)
assertresult==42

Advanced Usage (Kind-Based):

# Define custom "kinds" (not just types)graph.add_node('json_string', isa=lambdax: isinstance(x, str) andx.startswith('{'))
graph.add_node('config_dict', isa=lambdax: isinstance(x, dict))
@graph.register_edge('json_string', 'config_dict')defparse_json(text, ctx):
importjsonreturnjson.loads(text)
# Transform with automatic kind detectionresult=graph.transform('{"key": "value"}', 'config_dict', from_kind='json_string')

Key Features:

  • Multi-hop routing with cost-based path selection
  • Support for arbitrary hashable kinds (types, strings, custom markers)
  • Pluggable kind detection with predicates
  • Context propagation for dependency injection
  • MRO-aware fallback for type hierarchies

i2.signatures - Function Signature Manipulation

signatures provides a calculus for working with function signatures - introspecting, merging, and modifying them programmatically.

Signature Introspection:

fromi2.signaturesimportSigdeffunc(z, a: float=1.0, /, b=2, *, c: int=3):
passsig=Sig(func)
print(sig.names) # ['z', 'a', 'b', 'c']print(sig.defaults) # {'a': 1.0, 'b': 2, 'c': 3}print(sig.annotations) # {'a': <class 'float'>, 'c': <class 'int'>}

Signature Construction:

# From functionsig1=Sig(lambdax, y: x+y)
# From list of namessig2=Sig(['a', 'b', 'c'])
# From stringsig3=Sig('x y z')
# All create callable Signature objectsprint(sig2) # <Sig (a, b, c)>

Signature Merging:

deffoo(x, y=1): passdefbar(z: int, *, w=2): pass# Combine signaturescombined=Sig(foo) +Sig(bar)
print(combined) # <Sig (x, y=1, z: int, w=2)>

Decorating with Signatures:

# Give a function a specific signature@Sig('a b c')deffunc(*args, **kwargs):
print(f"Called with: {args}, {kwargs}")
# Now func has signature (a, b, c)func(1, 2, 3) # Works as expected

Key Features:

  • Extract parameter names, kinds, defaults, and annotations
  • Merge multiple signatures flexibly
  • Apply signatures as decorators
  • Support for all parameter kinds (positional-only, keyword-only, VAR_POSITIONAL, VAR_KEYWORD)
  • Signature algebra for composing function interfaces

i2.wrapper - Ingress/Egress Function Wrapping

wrapper provides the Wrap class for transforming function inputs and outputs through composable ingress/egress layers.

Basic Wrapping:

fromi2.wrapperimportWrapdefadd(x, y):
returnx+y# Transform inputs before function, outputs afterwrapped=Wrap(
add,
ingress=lambdax, y: (x*2, y*2), # Double inputsegress=lambdaresult: result/2# Halve output
)
result=wrapped(3, 4) # (3*2 + 4*2) / 2 = 7assertresult==7

Signature Transformation:

fromi2.wrapperimportIngressdefprocess(data: dict):
returndata['value']
# Change signature: accept 'x' instead of 'data'ingress=Ingress(
outer_sig='x',
inner_sig='data',
kwargs_trans=lambdax: {'data': {'value': x}}
)
new_func=ingress(process)
result=new_func(42) # Calls process({'value': 42})assertresult==42

The Wrap Flow:

*outer_args, **outer_kwargs
↓
[ingress] - transform inputs
↓
*inner_args, **inner_kwargs
↓
[func] - original function
↓
func_output
↓
[egress] - transform outputs
↓
final_output

Key Features:

  • Separate ingress (input transformation) and egress (output transformation)
  • Signature-aware argument mapping
  • Composable wrapper layers
  • Supports partial application and argument reordering
  • Clean separation of concerns for cross-cutting functionality

i2.routing_forest - Conditional Logic as Data Structures

routing_forest lets you express nested if/then conditions as composable, reusable tree structures instead of tangled code.

Basic Routing:

fromi2.routing_forestimportRoutingForest, CondNode, FinalNode# Define routing logic as a forestrouter=RoutingForest([
CondNode(
cond=lambdax: isinstance(x, int),
then=FinalNode("It's an integer!")
),
CondNode(
cond=lambdax: isinstance(x, str),
then=FinalNode("It's a string!")
)
])
# Get first matchresult=next(router(42))
assertresult=="It's an integer!"

Nested Conditions:

# Nested routing with multiple conditionsrouter=RoutingForest([
CondNode(
cond=lambdax: isinstance(x, (int, str)),
then=RoutingForest([
CondNode(
cond=lambdax: int(x) >=10,
then=FinalNode("≥ 10")
),
CondNode(
cond=lambdax: int(x) %2==1,
then=FinalNode("Odd number")
)
])
)
])
# Can get all matches or just firstlist(router(15)) # ['≥ 10', 'Odd number']next(router(8)) # None (no matches)

Pattern Matching Example:

# Router as pattern matcherdefroute_value(value):
router=RoutingForest([
CondNode(
cond=lambdax: x<0,
then=FinalNode("negative")
),
CondNode(
cond=lambdax: x==0,
then=FinalNode("zero")
),
CondNode(
cond=lambdax: x>0,
then=FinalNode("positive")
)
])
returnnext(router(value), "unknown")
assertroute_value(-5) =="negative"assertroute_value(0) =="zero"assertroute_value(10) =="positive"

Key Features:

  • Objectify nested if/then logic into composable components
  • Both callable and iterable nodes
  • Get first match, all matches, or default values
  • Cleaner than nested if/elif/else chains for complex routing
  • Reusable condition components

i2.util - Utility Functions and Helpers

util provides miscellaneous utility functions for common patterns.

Identity and Constant Functions:

fromi2.utilimportasis, return_true, return_false, return_none# Identity functionassertasis(42) ==42assertasis([1, 2, 3]) == [1, 2, 3]
# Constant functions (useful as defaults)assertreturn_true(anything, goes="here") isTrueassertreturn_false("doesn't", "matter") isFalseassertreturn_none(1, 2, 3) isNone

Object Naming:

fromi2.utilimportname_of_obj# Get name of various objectsassertname_of_obj(map) =='map'assertname_of_obj([1, 2, 3]) =='list'assertname_of_obj(lambdax: x) =='<lambda>'fromfunctoolsimportpartialassertname_of_obj(partial(print, sep=",")) =='print'

Attribute/Item Access:

fromi2.utilimportimdict# Flexible dict-like accessdata=imdict({'a': 1, 'b': 2})
assertdata.a==1# Attribute accessassertdata['b'] ==2# Item access

Laziness Utilities:

fromi2.utilimportlazypropclassDataLoader:
@lazypropdefexpensive_data(self):
print("Loading...")
return [1, 2, 3, 4, 5]
loader=DataLoader()
# First access computes and cachesdata1=loader.expensive_data# Prints "Loading..."# Subsequent accesses use cached valuedata2=loader.expensive_data# No printassertdata1isdata2

Key Features:

  • Common function patterns (identity, constants)
  • Object introspection helpers
  • Flexible attribute/item access wrappers
  • Lazy evaluation utilities
  • Deprecation helpers
  • String manipulation tools

Common Patterns

Composing Transformations

fromi2.castgraphimportTransformationGraphfromi2.wrapperimportWrap# Define transformation graphgraph=TransformationGraph()
@graph.register_edge('csv', 'rows')defparse_csv(text, ctx):
return [line.split(',') forlineintext.strip().split('\n')]
@graph.register_edge('rows', 'records')defrows_to_records(rows, ctx):
return [dict(zip(headers, row)) forrowinrows[1:]]
# Use with wrapper for clean APIdefprocess_csv(csv_text: str) ->list:
returngraph.transform(csv_text, 'records', from_kind='csv')
# Wrap to add validationvalidated=Wrap(
process_csv,
ingress=lambdatext: (text.strip(),),
egress=lambdarecords: [rforrinrecordsifr] # Filter empties
)

Dynamic Signature Manipulation

fromi2.signaturesimportSigfromi2.wrapperimportIngress# Start with a general functiondefprocess(**kwargs):
returnsum(kwargs.values())
# Give it a specific signature@Sig('a b c')deftyped_process(**kwargs):
returnprocess(**kwargs)
# Now can call with clear parametersresult=typed_process(1, 2, 3)
assertresult==6

Routing with Validation

fromi2.routing_forestimportRoutingForest, CondNode, FinalNodefromi2.utilimportreturn_nonedefvalidate_input(value):
"""Route to appropriate validator."""router=RoutingForest([
CondNode(
cond=lambdax: isinstance(x, str),
then=RoutingForest([
CondNode(lambdax: len(x) >0, FinalNode(True)),
CondNode(lambdax: len(x) ==0, FinalNode(False))
])
),
CondNode(
cond=lambdax: isinstance(x, int),
then=FinalNode(x>=0)
)
])
returnnext(router(value), False)
assertvalidate_input("hello") isTrueassertvalidate_input("") isFalseassertvalidate_input(5) isTrueassertvalidate_input(-1) isFalse

What's mint?

Mint stands for "Meta-INTerface".

Minting is core technique of i2i: It can be seen as the encapsulation of a construct’s interface into a (data) structure that contains everything one needs to know about the construct to perform a specific action with or on the construct.

A little note on the use of “encapsulation”. The term is widely used in computer science, and is typically tied to object oriented programming. Wikipedia provides two definitions:

  • A language mechanism for restricting direct access to some of the object's components.
  • A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data.

Though both these definitions apply to minting, the original sense of the word “encapsulate” is even more relevant (from google definitions):

  • express the essential features of (something) succinctly
  • enclose (something) in or as if in a capsule

Indeed, minting is the process of enclosing a construct into a “mint” (for “Meta INTerface”) that will express the features of the construct that are essential to the task at hand. The mint provides a declarative layer of the construct that allows one to write code that operates with this layer, which is designed to be (as) consistent (as possible) from one system/language to another.

For example, whether a (non-anonymous) function was written in C, Python, or JavaScript, it will at least have a name, and it's arguments will (most often) have names, and may have types. Similarly with "data objects": The data of both JavaScript and Python objects can be represented by a tree whose leaves are base types, which can in turn be represented by a C struct.

About

Python Mint creation, manipulation, and use

Resources

Stars

2 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages