Module loading
Sommaire
- CLI : -m
- Langage : import()
- Binding automatique
- Binding explicite
- Depuis un callback ND parallèle
- Ordre de résolution
- Protocoles
- Modules stdlib
- Noms dotted
- Imports relatifs
- Packages lib.toml
- Auto-import
- Wild import et import sélectif
- Wild import
- Import sélectif
- Modules Catnip et META
- Exports
- Métadonnées
- Cache
- Module Policy
- Policies nommées
- API Python
- Écrire un module host
- REPL avec modules
Catnip charge des modules Catnip, Python et Rust dans des namespaces explicites.
CLI : -m
catnip -m math script.cat
catnip -m math -m random script.cat
Deux suffixes contrôlent le nom exposé :
| Suffixe | Exemple | Effet |
|---|---|---|
:alias |
-m math:m |
Namespace renommé : m.sqrt() |
:! |
-m io:! |
Exports injectés directement dans les globals |
catnip -m math:m -c "m.sqrt(16)"
# ⇒ 4.0
catnip -m io:! -c "print('BORN TO SEGFAULT')"
# ⇒ BORN TO SEGFAULT
Sans suffixe, le namespace porte le dernier segment du nom du module.
Langage : import()
import() prend un nom de module et retourne un namespace.
Binding automatique
En position statement, un appel avec un seul argument lie automatiquement le module :
import('math')
math.sqrt(144)
# → 12.0
Le nom lié vient du dernier segment. Les noms contenant des segments internes, comme os.path, utilisent la forme
expression :
p = import('os.path')
p.join("a", "b")
# → "a/b"
Binding explicite
m = import('math')
m.sqrt(144)
# → 12.0
Depuis un callback ND parallèle
Le chargeur d'imports est lié au thread principal. Un callback ~>/~~ exécuté en mode thread — ou en mode process
retombé sur thread — ne peut donc pas importer, et le refus est explicite :
RuntimeError: import() is not supported inside a thread-parallel ND callback: the import loader is bound to the main
thread. Use pragma('nd_mode', 'sequential'), or hoist the import out of the callback.
Le refus porte sur l'appel, pas sur l'écriture du nom : il vaut donc aussi quand l'import est atteint à travers une fonction que le callback appelle, ou par un nom résolu à l'exécution. Les deux remèdes sont ceux du message — sortir l'import du callback, ce qui suffit dans la plupart des cas puisque le module reste visible par capture, ou renoncer au parallélisme pour ce broadcast.
Ordre de résolution
Pour import('name'), le loader cherche d'abord les fichiers et packages, puis les modules stdlib Rust, puis
importlib pour la stdlib Python et les packages installés. Avant d'exécuter le module résolu, il consulte la clé de
cache adaptée au backend.
Les répertoires de recherche sont parcourus dans cet ordre canonique :
- caller_dir : répertoire du fichier appelant, déterminé par
META.file; - CWD : répertoire courant du processus ;
CATNIP_PATH: répertoires supplémentaires, dans l'ordre de la variable.
export CATNIP_PATH="/opt/catnip-libs:$HOME/.catnip/modules"
catnip script.cat
En REPL et avec -c, META.file n'existe pas : la recherche commence donc au CWD, puis continue dans CATNIP_PATH.
Un fichier trouvé avant la stdlib peut masquer un module du même nom. Le kwarg protocol force un backend précis quand
ce masquage n'est pas souhaité.
La liste de recherche est fixée au lancement. Aucun module ne peut modifier le futur des imports trois frames plus haut.
Protocoles
protocol accepte trois valeurs :
| Valeur | Backend recherché |
|---|---|
"cat" |
Module .cat |
"py" |
Fichier .py ou module Python via importlib |
"rs" |
Extension native |
host = import('host', protocol='py')
tools = import('tools', protocol='cat')
ext = import('myext', protocol='rs')
Si utils.cat et utils.py coexistent, le fichier Catnip gagne sans protocole explicite :
import('utils')
import('utils', protocol='py')
import('utils', protocol='cat')
protocol="cat" bloque le fallback importlib. Une valeur inconnue lève CatnipRuntimeError.
Modules stdlib
Les modules natifs exposent PROTOCOL == "rust" :
Chacun existe en deux exemplaires : une extension Python, servie au CLI et à l'API, et un plugin natif, servi au moteur
pur — celui du serveur MCP et des embarqueurs sans Python. Les deux répondent la même chose au même code, ce qu'un test
de parité vérifie. Le plugin natif est cherché dans CATNIP_STDLIB_PATH, puis à côté de l'exécutable et dans son
lib/, où make install-bins le dépose. Il se construit pour Linux et macOS, les deux plateformes que le projet
distribue : il lit et écrit les descripteurs du processus directement. io.input() y lit le descripteur 0 sans tampon
qui lui survive — ce qu'un programme ne lit pas est perdu avec lui, et n'attend pas l'exécution suivante dans le même
processus.
Un module en deux exemplaires reste un module tant que personne ne les compare.
| Module | Exports principaux |
|---|---|
io |
print, write, writeln, eprint, input, open |
sys |
argv, environ, executable, version, platform, exit |
http |
get, post, put, delete, request, Server, serve, auth HTTP |
Le descripteur rendu par io.open() porte read, readline, write, close, les attributs name/mode/closed,
et le protocole de gestion de contexte — with f = io.open("data.txt") { f.read() } ferme le fichier à la sortie du
bloc, y compris quand celui-ci lève.
sys.exit() suit la convention Python : sans argument ou avec None il sort en succès, avec un entier il sort avec sa
valeur, et avec n'importe quoi d'autre il imprime l'objet sur la sortie d'erreur et sort en échec.
Les fonctions de module prennent les arguments nommés de leurs équivalents Python (sep, end, flush, code, et les
huit paramètres de open), par position comme par nom : la page des modules donne la liste et les
limites qui subsistent entre les deux implémentations.
Pour charger l'homonyme Python d'un module natif :
python_http = import('http', protocol='py')
Noms dotted
Le point sépare les segments de package :
m = import('mylib.utils')
# cherche mylib/utils.cat, mylib/utils.py ou un package compatible
p = import('PIL.Image')
# fallback importlib si aucun fichier local n'est trouvé
Imports relatifs
Les points initiaux résolvent le nom depuis le fichier appelant :
| Syntaxe | Résolution |
|---|---|
.utils |
caller_dir/utils.cat |
..utils |
caller_dir/../utils.cat |
...utils |
caller_dir/../../utils.cat |
..lib.utils |
caller_dir/../lib/utils.cat |
project/
main.cat
lib/
core.cat
helpers.cat
shared/
utils.cat
# lib/core.cat
helpers = import('.helpers')
utils = import('..shared.utils')
Contraintes :
-
META.fileest requis ; les imports relatifs ne fonctionnent pas en REPL ni avec-c; -
aucun fallback vers le CWD,
CATNIP_PATHouimportlib; -
protocolcontinue de filtrer le backend ; -
"."et".."seuls sont invalides ; -
"./foo"reste un chemin et n'est pas accepté comme nom de module ; -
depuis un package, la remontée s'arrête à sa racine. Un point de trop lève en nommant cette racine, au lieu de résoudre vers un fichier situé au-dessus :
text
relative import '...dehors' escapes its package
package root: /chemin/vers/pkg
Hors package, la remontée n'a pas de borne — rien ne définit de racine pour des .cat en vrac, et elle sature au
répertoire racine du système de fichiers. Un fichier destiné à être importé par des tiers gagne donc un lib.toml,
qui lui donne une frontière en même temps qu'un nom.
Packages lib.toml
Un répertoire contenant lib.toml est un package. Il a priorité sur un fichier homonyme comme mylib.cat.
[lib]
name = "mylib"
version = "0.1.0"
entry = "main.cat"
[lib.exports]
include = ["fn_a", "fn_b"]
mylib/
lib.toml
main.cat
helpers.cat
m = import('mylib')
h = import('mylib.helpers')
entry vaut main.cat par défaut. lib.exports.include restreint le namespace ; sans cette clé, les règles d'export
standard s'appliquent.
Un répertoire sans lib.toml n'est pas chargé comme package.
Auto-import
Sans configuration, io:! est chargé en CLI et en REPL. L'embedding ne charge rien automatiquement — print, input
et open y restent disponibles quand même, comme builtins du runtime ; write, writeln et eprint, non.
[modules]
auto = ["io"]
[modules.repl]
auto = ["io", "math"]
[modules.cli]
auto = ["io"]
[modules.dsl]
auto = []
La section du mode courant remplace [modules].auto. Les modules -m s'ajoutent ensuite, après déduplication.
Depuis Python :
from catnip import Catnip
cat = Catnip(auto=['io', 'math'])
cat.parse('math.sqrt(16)')
result = cat.execute()
# ⇒ 4.0
Un auto-import introuvable n'empêche pas le chargement des modules restants.
Wild import et import sélectif
Wild import
wild=True injecte tous les exports publics dans les globals et retourne None :
import('utils', wild=True)
double(5)
META et les noms préfixés par _ ne sont jamais injectés.
Import sélectif
Les arguments qui suivent le spec sélectionnent les exports. name:alias les renomme :
import('math', 'sqrt', 'pi:p')
sqrt(144)
# → 12.0
p
# → 3.141592653589793
L'import sélectif retourne aussi None. Il ne peut pas être combiné avec wild=True.
Modules Catnip et META
Chaque module et script reçoit un objet META.
Exports
Le loader choisit les exports dans cet ordre :
META.exports;__all__;- tous les noms sauf
METAet ceux préfixés par_.
add = (a, b) => { a + b }
sub = (a, b) => { a - b }
_helper = (x) => { x }
META.exports = list("add", "sub")
Métadonnées
Avant l'exécution, le loader renseigne :
META.file: chemin absolu du fichier ;META.main:Truepour un script exécuté directement ;META.protocol:"cat"pour un module Catnip.
Le code peut ajouter d'autres attributs à META.
Cache
Un module n'est exécuté qu'une fois par contexte. Les imports suivants retournent le même namespace.
La clé dépend du backend :
- fichiers
.cat,.pyet extensions natives : chemin absolu résolu ; - modules
importlibet stdlib : nom du module et protocole.
Deux fichiers homonymes situés dans des répertoires différents restent donc distincts, et import('sys') ne répond pas
pour import('sys', protocol='py') : ce sont deux modules, ils ont deux entrées.
Module Policy
Une policy contrôle les noms importables :
[modules]
policy = "deny"
allow = ["math", "json", "random", "numpy.*"]
deny = ["os", "subprocess", "sys", "importlib"]
L'évaluation applique :
deny;allow;- la valeur de fallback
policy.
deny gagne toujours. Le matching respecte les segments :
"os"matcheosetos.path;"os.*"matche les sous-modules, pasos;"oslo"ne matche pas"os".
Une policy filtre des noms, pas des effets. Autoriser un module l'autorise à faire ce qu'un module fait au
chargement — et un module qui déclare __catnip_extension__ reçoit le contexte et écrit dans ses globals (voir
Écrire un module host), donc il peut y remplacer import ou open. C'est la définition
d'une extension et non un trou de la policy, mais la conséquence se lit rarement dans l'autre sens : la liste allow
est une liste de code auquel on fait confiance, pas une liste de noms inertes. Pour une frontière qui ne repose pas sur
cette confiance, il faut une frontière de processus.
Policies nommées
[modules.policies.sandbox]
policy = "deny"
allow = ["math", "json", "io"]
[modules.policies.admin]
policy = "allow"
deny = ["subprocess", "os"]
catnip --policy sandbox script.cat
catnip module list-profiles
catnip module check sandbox os math json
API Python
from catnip import Catnip
from catnip._rs import ModulePolicy
policy = ModulePolicy('deny', allow=['math', 'json'], deny=['os'])
cat = Catnip(module_policy=policy)
La policy est héritée par les modules Catnip importés. Les imports relatifs sont contrôlés sous leur nom qualifié complet.
Les modules déjà en cache ne sont pas revérifiés quand une policy est installée après leur chargement.
Une policy réduit la surface d'import. Elle ne transforme pas un processus Python en frontière de sécurité.
Écrire un module host
Un module Python expose ses fonctions, classes et constantes publiques :
def double(value):
return value * 2
class Counter:
def __init__(self):
self.value = 0
def _private_helper():
return 'private'
double et Counter sont visibles ; _private_helper ne l'est pas.
Pour injecter des fonctions directement dans un Context ou distribuer une extension Catnip, voir
EXTENDING_CONTEXT.
REPL avec modules
catnip -m math
La présence de -m sélectionne la REPL Python afin que les namespaces chargés soient accessibles. Voir
REPL.