lib/generators: move limit detection into withRecursion

As suggested in #131205.

Now it's possible to pretty-print a value with `lib.generators` like
this:

    with lib.generators;
    toPretty { }
      (withRecursion { depthLimit = 10; } /* arbitrarily complex value */)

Also, this can be used for any other pretty-printer now if needed.
This commit is contained in:
Maximilian Bosch 2021-08-26 00:28:49 +02:00
parent b6d3c9f821
commit 5773ae93f7
No known key found for this signature in database
GPG Key ID: 091DBF4D1FC46B8E
3 changed files with 37 additions and 19 deletions

View File

@ -195,6 +195,30 @@ rec {
*/ */
toYAML = {}@args: toJSON args; toYAML = {}@args: toJSON args;
withRecursion =
args@{
/* If this option is not null, `toPretty` will stop evaluating at a certain depth */
depthLimit
/* If this option is true, an error will be thrown, if a certain given depth is exceeded */
, throwOnDepthLimit ? true
}:
assert builtins.isInt depthLimit;
let
transform = depth:
if depthLimit != null && depth > depthLimit then
if throwOnDepthLimit
then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to pretty-print with `generators.withRecursion'!"
else const "<unevaluated>"
else id;
mapAny = with builtins; depth: v:
let
evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
in
if isAttrs v then mapAttrs (const evalNext) v
else if isList v then map evalNext v
else transform (depth + 1) v;
in
mapAny 0;
/* Pretty print a value, akin to `builtins.trace`. /* Pretty print a value, akin to `builtins.trace`.
* Should probably be a builtin as well. * Should probably be a builtin as well.
@ -205,23 +229,14 @@ rec {
(This means fn is type Val -> String.) */ (This means fn is type Val -> String.) */
allowPrettyValues ? false, allowPrettyValues ? false,
/* If this option is true, the output is indented with newlines for attribute sets and lists */ /* If this option is true, the output is indented with newlines for attribute sets and lists */
multiline ? true, multiline ? true
/* If this option is not null, `toPretty` will stop evaluating at a certain depth */
depthLimit ? null,
/* If this option is true, an error will be thrown, if a certain given depth is exceeded */
throwOnDepthLimit ? false
}@args: }@args:
assert depthLimit != null -> builtins.isInt depthLimit;
assert throwOnDepthLimit -> depthLimit != null;
let let
go = depth: indent: v: with builtins; go = indent: v: with builtins;
let isPath = v: typeOf v == "path"; let isPath = v: typeOf v == "path";
introSpace = if multiline then "\n${indent} " else " "; introSpace = if multiline then "\n${indent} " else " ";
outroSpace = if multiline then "\n${indent}" else " "; outroSpace = if multiline then "\n${indent}" else " ";
in if depthLimit != null && depth > depthLimit then in if isInt v then toString v
if throwOnDepthLimit then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to pretty-print with `generators.toPretty'!"
else "<unevaluated>"
else if isInt v then toString v
else if isFloat v then "~${toString v}" else if isFloat v then "~${toString v}"
else if isString v then else if isString v then
let let
@ -243,7 +258,7 @@ rec {
else if isList v then else if isList v then
if v == [] then "[ ]" if v == [] then "[ ]"
else "[" + introSpace else "[" + introSpace
+ libStr.concatMapStringsSep introSpace (go (depth + 1) (indent + " ")) v + libStr.concatMapStringsSep introSpace (go (indent + " ")) v
+ outroSpace + "]" + outroSpace + "]"
else if isFunction v then else if isFunction v then
let fna = lib.functionArgs v; let fna = lib.functionArgs v;
@ -262,10 +277,10 @@ rec {
else "{" + introSpace else "{" + introSpace
+ libStr.concatStringsSep introSpace (libAttr.mapAttrsToList + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
(name: value: (name: value:
"${libStr.escapeNixIdentifier name} = ${go (depth + 1) (indent + " ") value};") v) "${libStr.escapeNixIdentifier name} = ${go (indent + " ") value};") v)
+ outroSpace + "}" + outroSpace + "}"
else abort "generators.toPretty: should never happen (v = ${v})"; else abort "generators.toPretty: should never happen (v = ${v})";
in go 0 ""; in go "";
# PLIST handling # PLIST handling
toPlist = {}: v: let toPlist = {}: v: let

View File

@ -247,7 +247,9 @@ rec {
showDefs = defs: concatMapStrings (def: showDefs = defs: concatMapStrings (def:
let let
# Pretty print the value for display, if successful # Pretty print the value for display, if successful
prettyEval = builtins.tryEval (lib.generators.toPretty { depthLimit = 10; } def.value); prettyEval = builtins.tryEval
(lib.generators.toPretty { }
(lib.generators.withRecursion { depthLimit = 10; throwOnDepthLimit = false; } def.value));
# Split it into its lines # Split it into its lines
lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value); lines = filter (v: ! isList v) (builtins.split "\n" prettyEval.value);
# Only display the first 5 lines, and indent them for better visibility # Only display the first 5 lines, and indent them for better visibility

View File

@ -534,8 +534,8 @@ runTests {
a.b = 1; a.b = 1;
a.c = a; a.c = a;
in { in {
expr = generators.toPretty { depthLimit = 2; } a; expr = generators.toPretty { } (generators.withRecursion { throwOnDepthLimit = false; depthLimit = 2; } a);
expected = "{\n b = 1;\n c = {\n b = 1;\n c = {\n b = <unevaluated>;\n c = <unevaluated>;\n };\n };\n}"; expected = "{\n b = 1;\n c = {\n b = \"<unevaluated>\";\n c = {\n b = \"<unevaluated>\";\n c = \"<unevaluated>\";\n };\n };\n}";
}; };
testToPrettyLimitThrow = testToPrettyLimitThrow =
@ -543,7 +543,8 @@ runTests {
a.b = 1; a.b = 1;
a.c = a; a.c = a;
in { in {
expr = (builtins.tryEval (generators.toPretty { depthLimit = 2; throwOnDepthLimit = true; } a)).success; expr = (builtins.tryEval
(generators.toPretty { } (generators.withRecursion { depthLimit = 2; } a))).success;
expected = false; expected = false;
}; };