Étendre le contexte
Sommaire
- Introduction
- Méthode 1 : Ajouter des fonctions simples
- Via le Context directement
- Via le constructeur de Context
- Méthode 2 : Fonctions avec accès au Context
- Méthode 3 : Classes et objets
- Méthode 4 : Modules complets
- Méthode 5 : extension distribuable (__catnip_extension__)
- Le descripteur
- Charger depuis Catnip : import()
- register(context) : au-delà du sac de valeurs
- Packager : entry point catnip.extensions
- Context manuel ou extension ?
- Exemples pratiques
- Exemple 1 : API de configuration
- Exemple 2 : Pipeline de traitement de données
- Exemple 3 : Système de règles métier
- Bonnes pratiques
- 1. Nommage cohérent
- 2. Documentation des fonctions
- 3. Validation des entrées
- 4. Isolation des contextes
- 5. Gestion des erreurs
- Context avancé : locals vs globals
- Logger personnalisé
- Résumé
Introduction
Catnip est volontairement sans I/O : pas d'accès réseau, fichiers, ni même print par défaut. Les fonctions I/O
(print, write, input) viennent du module io, auto-importé en CLI et REPL mais absent en embedded mode. Le host
expose ensuite ce dont l'application a besoin, au bon niveau de contrôle.
Kernighan, Ritchie et Thompson auraient probablement approuvé un cœur sans I/O.
Le point d'extension principal est le context (Context). Il fait le lien entre l'application et le runtime, et
transporte :
globals: dictionnaire de fonctions, classes et valeurs accessibles depuis Catniplocals: scope local (géré automatiquement par Catnip)result: dernier résultat (accessible viactx.resultcôté host)
Par défaut, le context expose des primitives de calcul (len, range, sorted...) et un logger minimal, mais aucune
fonction I/O. print, write et input sont fournis par le module io (auto-importé en CLI/REPL, configurable via
catnip.toml).
Méthode 1 : Ajouter des fonctions simples
Via le Context directement
from catnip import Catnip, Context
# Créer un context personnalisé
ctx = Context()
# Ajouter vos fonctions
def greet(name):
return f"Hello, {name}!"
def calculate_discount(price, percent):
return price * (1 - percent / 100)
ctx.globals['greet'] = greet
ctx.globals['discount'] = calculate_discount
# Utiliser le context avec Catnip
catnip = Catnip(context=ctx)
code = catnip.parse('greet("Alice")')
result = catnip.execute()
print(result) # "Hello, Alice!"
Via le constructeur de Context
from catnip import Context, Catnip
# Définir vos fonctions
def add_tax(price, rate=0.2):
return price * (1 + rate)
def format_price(amount):
return f"{amount:.2f}€"
# Créer le context avec vos fonctions
custom_globals = {
'add_tax': add_tax,
'format_price': format_price,
}
ctx = Context(globals=custom_globals)
catnip = Catnip(context=ctx)
# Utiliser depuis Catnip
code = catnip.parse('format_price(add_tax(100))')
result = catnip.execute()
print(result) # "120.00€"
Méthode 2 : Fonctions avec accès au Context
Pour les fonctions qui ont besoin d'accéder au context (pour lire/modifier des variables, accéder au résultat précédent,
etc.), utilisez le décorateur @pass_context.
from catnip import Catnip, Context, pass_context
@pass_context
def store(ctx, key, value):
"""Stocke une valeur dans le context global."""
ctx.globals[key] = value
return value
@pass_context
def recall(ctx, key):
"""Récupère une valeur depuis le context global."""
return ctx.globals.get(key, None)
@pass_context
def get_last_result(ctx):
"""Récupère le dernier résultat calculé."""
return ctx.result
# Configuration
ctx = Context()
ctx.globals['store'] = store
ctx.globals['recall'] = recall
ctx.globals['last'] = get_last_result
catnip = Catnip(context=ctx)
# Utilisation
catnip.parse('store("user_score", 42)')
catnip.execute()
catnip.parse('recall("user_score") + 8')
result = catnip.execute()
print(result) # 50
Méthode 3 : Classes et objets
Vous pouvez exposer des classes et des objets complets à Catnip.
from catnip import Catnip, Context
class DataStore:
def __init__(self):
self.data = {}
def set(self, key, value):
self.data[key] = value
return value
def get(self, key, default=None):
return self.data.get(key, default)
def all(self):
return self.data
# Créer une instance
store = DataStore()
# Exposer l'objet
ctx = Context()
ctx.globals['db'] = store
catnip = Catnip(context=ctx)
# Utiliser depuis Catnip
catnip.parse('db.set("name", "Alice")')
catnip.execute()
catnip.parse('db.get("name")')
result = catnip.execute()
print(result) # "Alice"
catnip.parse('db.all()')
result = catnip.execute()
print(result) # {'name': 'Alice'}
Méthode 4 : Modules complets
Pour exposer des modules Python entiers (comme math, datetime, etc.) :
from catnip import Catnip, Context
import math
import datetime
ctx = Context()
# Exposer un module entier
ctx.globals['math'] = math
ctx.globals['datetime'] = datetime
catnip = Catnip(context=ctx)
# Utiliser les fonctions du module
catnip.parse('math.sqrt(16)')
result = catnip.execute()
print(result) # 4.0
catnip.parse('math.pi * 2')
result = catnip.execute()
print(result) # 6.283185307179586
Méthode 5 : extension distribuable (__catnip_extension__)
Les méthodes 1 à 4 câblent le Context dans l'application hôte, avant exécution. Parfait quand le host contrôle le
runtime — mais ça ne se distribue pas : impossible de publier un paquet qu'un autre projet « branche » sans réécrire le
câblage à la main.
Une extension renverse le sens. C'est un module Python qui se déclare lui-même extensible Catnip, via un attribut
__catnip_extension__. Au moment d'un import(...), le runtime le détecte, appelle son hook et injecte ce qu'il
exporte. Un entry point pip ne déclenche pas ce chargement : il rend seulement l'extension découvrable par la CLI
(catnip extensions list / info).
Une extension est un module qui se sait importable. Le Context, lui, ignore jusqu'à sa propre existence.
Le descripteur
__catnip_extension__ est un dict :
'name'(str, requis) — identifiant de l'extension'version'(str, requis)'description'(str, optionnel)'register'(callable, optionnel) — hook appelé avec un proxy transparent vers leContext'exports'(dict, optionnel) — noms injectés danscontext.globals
Un module extension complet tient en quelques lignes :
# greetings_ext.py -- une extension Catnip distribuable
def _greet(name):
return f"Hello, {name}!"
def _register(context):
# Le hook reçoit un proxy transparent vers le Context : effet de bord direct + lecture d'attributs réels.
context.globals['greeting_loaded'] = True
if context.logger is not None:
context.logger.info("greetings extension ready")
__catnip_extension__ = dict(
name='greetings',
version='1.0.0',
description="Salutations pour Catnip",
register=_register,
exports=dict(greet=_greet, EXCLAIM='!'),
)
Charger depuis Catnip : import()
import("module") détecte le descripteur, exécute register, puis injecte exports. Les noms exportés sont
immédiatement disponibles dans le scope :
from catnip import Catnip
c = Catnip()
c.parse('import("greetings_ext"); greet("world")')
print(c.execute()) # Hello, world!
Côté script Catnip, c'est un import ordinaire — la détection est transparente :
import("greetings_ext")
greet("Catnip") # "Hello, Catnip!"
Les exports écrasent les globals préexistants de même nom (aligné sur l'API Python). L'extension est suivie dans
context._extensions. Dans un même contexte, le module est mis en cache : un second import renvoie le namespace déjà
chargé sans relancer register ni réinjecter exports. Une nouvelle instance Catnip (cache neuf) recharge
l'extension avec les valeurs courantes du descripteur.
register(context) : au-delà du sac de valeurs
exports couvre le cas simple : un dictionnaire de noms à exposer. register sert quand l'extension a besoin du
Context lui-même. Le hook reçoit un proxy transparent : écrire dans context.globals atteint à la fois la VM et les
globals réels, et tout autre attribut (context.logger, etc.) délègue au vrai Context — pas une copie appauvrie. Les
effets de bord d'un register sont identiques en mode VM et en mode AST.
exportsdéclare ce que l'extension donne.registeragit sur ce qu'elle touche. Les deux sont optionnels, mais une extension sans ni l'un ni l'autre n'étend rien.
Packager : entry point catnip.extensions
Pour qu'une extension soit installable et découvrable sans import() explicite, déclarer un entry point dans le groupe
catnip.extensions :
[project.entry-points."catnip.extensions"]
greetings = "greetings_ext"
Une fois le paquet installé, la CLI inspecte ce qui est disponible :
catnip extensions list # name + entry_point
catnip extensions info greetings # name, version, register?, exports
L'entry point alimente cette inspection CLI, pas le chargement : pour utiliser l'extension dans un script, il faut
toujours import("greetings_ext").
Context manuel ou extension ?
| Critère | Méthodes 1-4 (Context) | Méthode 5 (extension) |
|---|---|---|
| Qui câble | l'application hôte | le module lui-même |
| Distribution | non (code local) | oui (paquet pip + entry point) |
| Chargement | avant exécution | import(...) |
| Découverte CLI | non | entry point (extensions list) |
Accès Context |
direct (vous le construisez) | via register(context) |
Les deux coexistent : un host peut construire son Context (méthodes 1-4) et charger des extensions tierces
par-dessus.
Exemples pratiques
Exemple 1 : API de configuration
from catnip import Catnip, Context
class Config:
def __init__(self):
self.settings = {
'debug': False,
'port': 8080,
'host': 'localhost'
}
def get(self, key):
return self.settings.get(key)
def set(self, key, value):
self.settings[key] = value
return value
def update(self, **kwargs):
self.settings.update(kwargs)
return self.settings
config = Config()
ctx = Context()
ctx.globals['config'] = config
catnip = Catnip(context=ctx)
# L'utilisateur peut configurer via Catnip
catnip.parse('''
config.set("port", 3000)
config.set("debug", True)
''')
catnip.execute()
print(config.settings) # {'debug': True, 'port': 3000, 'host': 'localhost'}
Exemple 2 : Pipeline de traitement de données
from catnip import Catnip, Context
def load_data(source):
"""Simule le chargement de données."""
return [1, 2, 3, 4, 5]
def transform(data, operation):
"""Applique une transformation."""
if operation == 'double':
return [x * 2 for x in data]
elif operation == 'square':
return [x ** 2 for x in data]
return data
def aggregate(data, func_name):
"""Agrège les données."""
if func_name == 'sum':
return sum(data)
elif func_name == 'avg':
return sum(data) / len(data)
return data
ctx = Context()
ctx.globals['load'] = load_data
ctx.globals['transform'] = transform
ctx.globals['aggregate'] = aggregate
catnip = Catnip(context=ctx)
# Pipeline défini en Catnip
pipeline = '''
data = load("source")
data = transform(data, "double")
aggregate(data, "sum")
'''
code = catnip.parse(pipeline)
result = catnip.execute()
print(result) # 30 (sum of [2, 4, 6, 8, 10])
Exemple 3 : Système de règles métier
from catnip import Catnip, Context, pass_context
class RuleEngine:
def __init__(self):
self.rules = []
def add_rule(self, name, condition_code, action_code):
self.rules.append({
'name': name,
'condition': condition_code,
'action': action_code
})
def evaluate(self, catnip, data):
"""Évalue les règles pour un ensemble de données."""
results = []
for rule in self.rules:
# Mettre les données dans le context
catnip.context.globals['data'] = data
# Évaluer la condition
catnip.parse(rule['condition'])
condition_result = catnip.execute()
if condition_result:
# Exécuter l'action
catnip.parse(rule['action'])
action_result = catnip.execute()
results.append({
'rule': rule['name'],
'result': action_result
})
return results
# Configuration
engine = RuleEngine()
ctx = Context()
ctx.globals['engine'] = engine
catnip = Catnip(context=ctx)
# Définir des règles
engine.add_rule(
'discount_high_value',
'data.price > 100',
'data.price * 0.9' # 10% discount
)
engine.add_rule(
'discount_quantity',
'data.quantity >= 10',
'data.price * 0.85' # 15% discount
)
# Évaluer
data = {'price': 120, 'quantity': 5}
results = engine.evaluate(catnip, data)
print(results) # [{'rule': 'discount_high_value', 'result': 108.0}]
data = {'price': 80, 'quantity': 15}
results = engine.evaluate(catnip, data)
print(results) # [{'rule': 'discount_quantity', 'result': 68.0}]
Bonnes pratiques
1. Nommage cohérent
# BON : noms descriptifs
ctx.globals['calculate_tax'] = calculate_tax
ctx.globals['format_date'] = format_date
ctx.globals['validate_email'] = validate_email
# ÉVITER : noms cryptiques
ctx.globals['ct'] = calculate_tax
ctx.globals['fd'] = format_date
2. Documentation des fonctions
def process_payment(amount, method):
"""
Traite un paiement.
Args:
amount (float): Montant du paiement
method (str): Méthode de paiement ('card', 'cash', 'paypal')
Returns:
dict: Résultat du paiement avec status et transaction_id
"""
return {'status': 'success', 'transaction_id': '12345'}
3. Validation des entrées
def divide(a, b):
"""Division sécurisée."""
if b == 0:
raise ValueError("Division par zéro interdite")
return a / b
ctx.globals['divide'] = divide
4. Isolation des contextes
Pour des environnements d'exécution multiples (multi-tenant, sandbox, etc.) :
def create_sandbox_context():
"""Crée un context isolé avec des fonctions limitées."""
ctx = Context(globals={
# Seulement les fonctions sûres
'len': len,
'str': str,
'int': int,
'range': range,
})
return ctx
# Chaque utilisateur a son propre context
user1_ctx = create_sandbox_context()
user2_ctx = create_sandbox_context()
user1_catnip = Catnip(context=user1_ctx)
user2_catnip = Catnip(context=user2_ctx)
5. Gestion des erreurs
from catnip import Catnip, Context
def safe_execute(catnip, code):
"""Exécute du code Catnip avec gestion d'erreurs."""
try:
catnip.parse(code)
return {'success': True, 'result': catnip.execute()}
except Exception as e:
return {'success': False, 'error': str(e)}
ctx = Context()
catnip = Catnip(context=ctx)
result = safe_execute(catnip, '1 + 1')
print(result) # {'success': True, 'result': 2}
result = safe_execute(catnip, 'unknown_function()')
print(result) # {'success': False, 'error': '…'}
Context avancé : locals vs globals
globals: Variables et fonctions disponibles partoutlocals: Variables de scope local (fonction, bloc)
from catnip import Catnip, Context
ctx = Context()
# Variables globales
ctx.globals['PI'] = 3.14159
ctx.globals['TAX_RATE'] = 0.2
catnip = Catnip(context=ctx)
code = '''
calculate_total = (price) => {
tax = price * TAX_RATE
price + tax
}
calculate_total(100)
'''
catnip.parse(code)
result = catnip.execute()
print(result) # 120.0
Logger personnalisé
Catnip expose un objet logger et une fonction debug() dans tous les contextes. Par défaut, un MinimalLogger
affiche les messages avec un préfixe [DEBUG], [INFO], etc.
Pour un contrôle total du logging, fournir un logger personnalisé au Context :
from catnip import Catnip
from catnip.context import Context
class CustomLogger:
"""Logger personnalisé pour Catnip."""
def print(self, *args, sep=' '):
msg = sep.join(str(arg) for arg in args)
# Intégration avec votre système de logging
my_app_logger.debug(msg)
def info(self, *args, sep=' '):
msg = sep.join(str(arg) for arg in args)
my_app_logger.info(msg)
def warning(self, *args, sep=' '):
msg = sep.join(str(arg) for arg in args)
my_app_logger.warning(msg)
def error(self, *args, sep=' '):
msg = sep.join(str(arg) for arg in args)
my_app_logger.error(msg)
# Créer le context avec le logger custom
ctx = Context(logger=CustomLogger())
catnip = Catnip(context=ctx)
# Utiliser depuis Catnip
code = '''
print("Démarrage du traitement")
logger.info("Configuration chargée")
resultat = 42
logger.debug("Résultat:", resultat)
'''
catnip.parse(code)
catnip.execute()
Note : Le logger est toujours exposé via logger et debug() dans les globals, même si tu fournis un dictionnaire
globals personnalisé au Context.
Résumé
Catnip offre plusieurs façons d'étendre ses capacités :
- Fonctions simples :
ctx.globals['func'] = my_function - Fonctions avec context :
@pass_contextdecorator - Objets et classes :
ctx.globals['obj'] = my_object - Modules complets :
ctx.globals['math'] = math - Logger personnalisé :
Context(logger=custom_logger)
Le design minimaliste de Catnip permet de créer des DSL (Domain-Specific Languages) sur mesure pour votre application, en exposant uniquement les fonctionnalités nécessaires.