2626 DataOp ,
2727 Scoring ,
2828 SplitX ,
29+ UninitializedVariable ,
2930 Value ,
3031 Var ,
3132)
32- from ._utils import NULL , X_NAME , Y_NAME , simple_repr
33+ from ._utils import IS_PREVIEW_DATA_ENV_NAME , NULL , X_NAME , Y_NAME , simple_repr
3334
3435_BUILTIN_SEQ = (list , tuple , set , frozenset )
3536
@@ -426,15 +427,15 @@ def _check_environment(environment):
426427 )
427428 # Notes about checking the env keys:
428429 #
429- # - env ⊂ variables: in some cases we could check that there are no extra
430- # keys in `environment`, ie all keys in `environment` correspond to a
431- # name in the DataOp. However in other cases we naturally end up
432- # using a bigger environment than what is needed. For example we want tu
433- # evaluate a sub-DataOp (such as the `mark_as_X ()` node), and to do
434- # it we use the environment that was passed to evaluate the full
435- # DataOp. So if we want such a verification it should be a separate
436- # check done at a higher level (eg in the estimators' `fit`, `predict`
437- # etc.) where we know we are not working with a sub-DataOp .
430+ # - env ⊂ variables: we could check that there are no extra keys in
431+ # `environment`, i.e. all keys in `environment` correspond to a name in
432+ # the DataOp. However in some cases we naturally end up using a bigger
433+ # environment than what is needed. For example we want to evaluate a
434+ # sub-DataOp (such as the result of `.skb.find ()` or `.skb.find_X_y()`),
435+ # and to do it we use the environment created to evaluate the full
436+ # DataOp. We do perform this check when a key is missing from the env to
437+ # provide a better error message, but it is only used for the content of
438+ # the message rather than enforcing no extra keys ahead of time .
438439 #
439440 # - variables ⊂ env: we cannot check that all variables in the DataOp
440441 # have a matching key in the `environment`, because depending on the
@@ -450,7 +451,14 @@ def _check_environment(environment):
450451# consistent with .skb.eval() in evaluate's params
451452
452453
453- def evaluate (data_op , mode = "preview" , environment = None , clear = False , callbacks = ()):
454+ def evaluate (
455+ data_op ,
456+ mode = "preview" ,
457+ environment = None ,
458+ clear = False ,
459+ callbacks = (),
460+ ancestor_data_op = None ,
461+ ):
454462 """Evaluate a DataOp.
455463
456464 Parameters
@@ -475,6 +483,14 @@ def evaluate(data_op, mode="preview", environment=None, clear=False, callbacks=(
475483 Each will be called, in the provided order, after evaluating each node.
476484 The signature is callback(data_op, result) where data_op is the DataOp
477485 that was just evaluated and result is the resulting value.
486+
487+ ancestor_data_op : DataOp or None
488+ When we are evaluating a part of a DataOp (e.g. the X node only), pass
489+ this so that we can look at all the variable names of the full DataOp
490+ when producing error messages about missing or extra keys in the
491+ environment. It is not used for other purposes than inspecting the
492+ variable an choice names it contains. In particular it is not
493+ evaluated.
478494 """
479495 _check_environment (environment )
480496 if clear :
@@ -486,11 +502,57 @@ def evaluate(data_op, mode="preview", environment=None, clear=False, callbacks=(
486502 return _Evaluator (mode = mode , environment = environment , callbacks = callbacks ).run (
487503 data_op
488504 )
505+ except UninitializedVariable as e :
506+ if (
507+ hasattr (e , "add_note" )
508+ and environment is not None
509+ and not environment .get (IS_PREVIEW_DATA_ENV_NAME )
510+ ):
511+ # user passed an explicit environment rather than using the
512+ # variables' preview values.
513+ e .add_note (
514+ _uninitialized_variable_msg (
515+ e ,
516+ ancestor_data_op if ancestor_data_op is not None else data_op ,
517+ environment ,
518+ )
519+ )
520+ raise
489521 finally :
490522 if clear :
491523 clear_results (data_op , mode = mode )
492524
493525
526+ def _uninitialized_variable_msg (error , data_op , environment ):
527+ missing_name = error .name
528+ var_names = list (named_nodes (data_op ).keys ())
529+ choice_names = [n for c in choices (data_op ).values () if (n := c .name ) is not None ]
530+ unused = list (
531+ {
532+ k
533+ for k in environment .keys ()
534+ if isinstance (k , str ) and not k .startswith ("_skrub_" )
535+ }.difference (var_names + choice_names )
536+ )
537+
538+ msg = (
539+ "- Note that preview values passed to initialize skrub variables\n "
540+ " are ignored by default whenever we pass "
541+ "an explicit 'environment' dictionary,\n "
542+ " for example when calling SkrubLearner.fit({'X': ..., 'y': ...}).\n "
543+ f" Please pass a value for { missing_name !r} in the environment.\n "
544+ " You can also use "
545+ f"skrub.var({ missing_name !r} , value=..., becomes_default=True)\n "
546+ " to always retain the initialization value as a default."
547+ )
548+ if unused :
549+ msg += (
550+ "\n - WARNING: the following keys were passed in the environment but "
551+ f"have no corresponding variable in the DataOp:\n { unused } "
552+ )
553+ return msg
554+
555+
494556def _cache_pruner (data_op , mode ):
495557 g = graph (data_op )
496558 ref_counts = {node_id : len (parents ) for node_id , parents in g ["parents" ].items ()}
@@ -675,6 +737,12 @@ def nodes(data_op):
675737 return list (graph (data_op )["nodes" ].values ())
676738
677739
740+ def named_nodes (data_op ):
741+ return {
742+ name : op for op in nodes (data_op ) if (name := op ._skrub_impl .name ) is not None
743+ }
744+
745+
678746def clear_results (data_op , mode = None ):
679747 for n in nodes (data_op ):
680748 if mode is None :
0 commit comments