-
-
Notifications
You must be signed in to change notification settings - Fork 14.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lib.types: attrsWith named placeholder #344216
Conversation
0e279d8
to
52f296e
Compare
LGTM |
8176af5
to
e4f51be
Compare
Seems like a wasted opportunity actually. The type could accept a function from name to type so you really have a name, that you can even feed into the submodule specialArgs using a different name name, etc. Anyway, I guess what I'm getting at is that the attrs types should be factored into a single more capable function, because this property is not mutually exclusive with the other property about laziness ( See also I'll add that to the description as well, because this would close that. |
So before i start factoring this, and make sure the checks pass. Do you mean something like this? # Interface
# elemTypeFn :: String -> OptionType
namedAttrsOf = attrName: elemTypeFn: mkOptionType rec {
# ...
} # Simple Usage with submodule taking a function itself
# I choose username as concrete name name here.
# Couldn't make up a nice name for `namedAttr` but `name` is probably bad, since it already used in types.submodule in a different way.
mkOption {
type = namedAttrsOf "username" (attrName: submoduleWith {
specialArgs = {
inherit attrName;
};
modules = [
# ... other modules receiving the name
];
}
);
}; |
I was thinking something along the lines of attrsWith {
name = "username";
itemType = submoduleWith { modules = [ <...> ]; };
# or, perhaps later:
# itemTypeFunction = name: submoduleWith { modules = f name; specialArgs = g name; };
# and perhaps later:
# lazy = true;
}; This is more extensible and will let us cover Also, instead of |
6432843
to
dc45ca2
Compare
@roberth okay. I refactored everthing accordingly. Lets see if all checks pass. Also when looking at the usage. It might be more consistent if we name
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In summary
- This would be a good opportunity to fix
showOption
properly - Some microoptimization
lib/options.nix
Outdated
# If the part is a named placeholder of the form "<...>" don't escape it. | ||
# Required for compatibility with: namedAttrsOf | ||
# Can lead to misleading escaping if somebody uses literally "<...>" in their option names. | ||
# This is the trade-off to allow for named placeholders in option names. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we actually have two types of option paths: abstract ones containing placeholders, and real ones that are just data.
Treating them the same isn't quite correct, and merging a workaround could complicate a real fix.
Also note that some of these path items are not module options but attrsOf attributes, and "*"
will occur as an attribute name in certain configurations to represent a "pattern" that matches everything; for example when an HTTP server doesn't responds for an unknown virtual host, etc.
We could solve this in at least two ways
a. Leave the "option path" type as is, but use two functions
- leave
showOption
as is, suitable for concrete option paths - add
showAbstractOption
, which implements these new rules
b. Only improve the representation - represent abstract path items with a value like
{ _type = "optionPathPlaceholder"; metaVariable = "name"; __toString = this: "<${this.metaVariable}>"; }
I think (b) may have good backward compatibility and it solves the problem for module options; not just attrsOf keys.
__toString
is mostly for compatibility with existing code that kind of works when a placeholder string is passed. This happens when generating option docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pre-existing tech debt, so while this is a good opportunity to fix it before making it slightly worse, perhaps this should be done in a follow-up PR instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to do this in a follow up PR if you are okay with it. Should i remove the changes from this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, nice work!
In addition to the issue I'm pointing out below, this should also have docs in https://nixos.org/manual/nixos/stable/#sec-option-types-composed. Otherwise I think this is good :)
lib/types.nix
Outdated
substSubModules = m: lazyAttrsOf (elemType.substSubModules m); | ||
functor = (defaultFunctor name) // { wrapped = elemType; }; | ||
substSubModules = m: attrsWith { elemType = elemType.substSubModules m; inherit name lazy; }; | ||
functor = (defaultFunctor typeName) // { wrapped = elemType; }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type merging is not well behaved right now. Here's the start of a test suite for that:
(import ./lib).evalModules {
modules = [
{
imports = [
({ lib, ... }: {
options.mergedLazy = lib.mkOption {
type = lib.types.attrsWith {
lazy = true;
elemType = lib.types.int;
};
};
})
({ lib, ... }: {
options.mergedLazy = lib.mkOption {
type = lib.types.attrsWith {
lazy = true;
elemType = lib.types.int;
};
};
})
({ config, ... }: {
# Can only evaluate if lazy
mergedLazy.bar = config.mergedLazy.baz + 1;
mergedLazy.baz = 10;
})
];
}
{
imports = [
({ lib, ... }: {
options.mergedName = lib.mkOption {
type = lib.types.attrsWith {
name = "id";
elemType = lib.types.submodule {
options.nested = lib.mkOption {};
};
};
};
})
({ lib, ... }: {
options.mergedName = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {});
};
})
({ lib, options, ... }: {
options.nameWhenMerged = lib.mkOption {
default = (options.mergedName.type.getSubOptions options.mergedName.loc).nested.loc;
};
})
];
}
];
}
Merging of lazy = true
doesn't work:
$ nix-instantiate --eval test.nix -A config.mergedLazy --strict
error: infinite recursion encountered
lazy
should only be merged successfully if the values of both types are the same.
Merging of name
doesn't work:
$ nix-instantiate --eval test.nix -A config.nameWhenMerged --strict
[ "mergedName" "<name>" "nested" ]
This should return <id>
instead. name
should be merged by prioritising non-default values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. Didnt think that the merging is done by <name>
literally. Thanks for the test suite, i'll try to fix the merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@infinisil I am unsure how to solve the option name merging. Are we sure this is a valid module definition?
I think to fix this we need to change how modules.mergeOptionDecls
works?
# Option definition 1
({ lib, ... }: {
options.mergedName = lib.mkOption {
type = lib.types.attrsWith {
name = "id";
elemType = lib.types.submodule {
options.nested = lib.mkOption {};
};
};
};
})
# Option definition 2 (same option, same type, different <name> placeholder)
({ lib, ... }: {
options.mergedName = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {});
};
})
And we might also need to make sure merging nested paths and conflicting placeholders
e.g. [<bar> <name> <name>]
[<name> <foo> <baz>]
[<foo> <name> <name>]
-> [<?> <foo> <baz>]
So basically we need to decide which of the two functions we will take, when merging two options declarations?
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${name}>"]);
(name = "id"
)
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${name}>"]);
(name = "name"
)
I couldnt find a way yet. But if you have a solution i'd be very thankfull.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and pushed a commit with how I think it should be implemented, I hope you don't mind!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Big thanks for doing the name merging.
I was confused if i was supposed to add additional attributes to the functor.
a77f68f
to
3bd01bc
Compare
@infinisil Just added some little documentation and fixed the missing functor attributes ( |
#344216 (comment) really needs to be added as tests, because those |
@infinisil Was somehow still stuck with the name merging.
Then in ...
else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
then f.type wrapped
# value types
else if (f.payload != null && f'.payload != null) && (payload != null)
then f.type payload This means if we omit I found a possible solution: To switch the order of # value types
else if (f.payload != null && f'.payload != null) && (payload != null)
then f.type payload
# composed types
else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
then f.type wrapped I also noticed that maybe both # value types
else if (f.payload != null && f'.payload != null)
# both f and f' have payload, If merged payload is null we should return null and not try f.type wrapped instead because the merge of payload failed already.
then
if payload != null
then f.type payload
else null
# composed types
else if (f.wrapped != null && f'.wrapped != null)
# same with wrapped
then
if wrapped != null
then f.type wrapped
else null |
e94f1ae
to
ebe48f1
Compare
@infinisil @roberth what do you think about this. I have this questions still in mind:
|
This should be considered in the context of also having e.g. https://github.com/NixOS/nixpkgs/pull/218812/files, which is a laterally related but independent parameter. This is somewhat of a red herring though, because we have good reasons to prefer a different name name for other reasons. Picking good names makes a big difference in the UX, as they're used over and over.
This means we have to often qualify the name of name, which requires more thinking than just having separate terms.
Compare:
Compare:
I think As for the other two points, I think @infinisil has more experience with type merging. |
@infinisil If we remove error:
… while calling anonymous lambda
at /nix/store/n9qgjvaqaipfi7y5skc0r1l27y487axh-nixos/lib/eval-cacheable-options.nix:1:1:
1| { libPath
| ^
2| , pkgsLibPath
… from call site
at /nix/store/n9qgjvaqaipfi7y5skc0r1l27y487axh-nixos/lib/make-options-doc/default.nix:130:17:
129| filteredOpts = lib.filter (opt: opt.visible && !opt.internal) transformedOpts;
130| optionsList = lib.flip map filteredOpts
| ^
131| (opt: opt
… while calling 'flip'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/trivial.nix:317:16:
316| */
317| flip = f: a: b: f b a;
| ^
318|
… from call site
at /nix/store/n9qgjvaqaipfi7y5skc0r1l27y487axh-nixos/lib/make-options-doc/default.nix:127:13:
126| let
127| rawOpts = lib.optionAttrSetToDocList options;
| ^
128| transformedOpts = map transformOptions rawOpts;
… while calling 'optionAttrSetToDocList''
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:320:32:
319|
320| optionAttrSetToDocList' = _: options:
| ^
321| concatMap (opt:
… while calling anonymous lambda
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:321:16:
320| optionAttrSetToDocList' = _: options:
321| concatMap (opt:
| ^
322| let
… from call site
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:358:26:
357| # builtins.trace opt.loc
358| [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options);
| ^
359|
… while calling 'optionals'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/lists.nix:820:5:
819| cond:
820| elems: if cond then elems else [];
| ^
821|
… from call site
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:353:31:
352| let ss = opt.type.getSubOptions opt.loc;
353| in if ss != {} then optionAttrSetToDocList' opt.loc ss else [];
| ^
354| subOptionsVisible = docOption.visible && opt.visible or null != "shallow";
… while calling 'optionAttrSetToDocList''
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:320:32:
319|
320| optionAttrSetToDocList' = _: options:
| ^
321| concatMap (opt:
… from call site
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/options.nix:358:67:
357| # builtins.trace opt.loc
358| [ docOption ] ++ optionals subOptionsVisible subOptions) (collect isOption options);
| ^
359|
… while calling 'collect'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/attrsets.nix:867:5:
866| pred:
867| attrs:
| ^
868| if pred attrs then
… while calling 'collect'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/attrsets.nix:867:5:
866| pred:
867| attrs:
| ^
868| if pred attrs then
… from call site
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/attrsets.nix:868:8:
867| attrs:
868| if pred attrs then
| ^
869| [ attrs ]
… while calling 'isType'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/types.nix:76:18:
75| rec {
76| isType = type: x: (x._type or "") == type;
| ^
77|
… from call site
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/types.nix:931:32:
930| # is just to avoid conflicts with potential options from the submodule
931| _freeformOptions = freeformType.getSubOptions prefix;
| ^
932| };
… while calling 'getSubOptions'
at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/types.nix:618:23:
617| emptyValue = { value = {}; };
618| getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${placeholder}>"]);
| ^
619| getSubModules = elemType.getSubModules;
error: value is null while a set was expected
Cacheable portion of option doc build failed.
Usually this means that an option attribute that ends up in documentation (eg `default` or `description`) depends on the restricted module arguments `config` or `pkgs`.
Rebuild your configuration with `--show-trace` to find the offending location. Remove the references to restricted arguments (eg by escaping their antiquotations or adding a `defaultText`) or disable the sandboxed build for the failing module by setting `meta.buildDocsInSandbox = false`.
error: builder for '/nix/store/0w0qq6927cvy20qmlzdr5ydsxr91f42a-lazy-options.json.drv' failed with exit code 1;
last 20 log lines:
> 930| # is just to avoid conflicts with potential options from the submodule
> 931| _freeformOptions = freeformType.getSubOptions prefix;
> | ^
> 932| };
>
> … while calling 'getSubOptions'
>
> at /nix/store/cr32l4lf8qjmx7vnfkfqqgxx7dfdvx7k-lib/types.nix:618:23:
>
> 617| emptyValue = { value = {}; };
> 618| getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${placeholder}>"]);
> | ^
> 619| getSubModules = elemType.getSubModules;
>
> error: value is null while a set was expected
> Cacheable portion of option doc build failed.
> Usually this means that an option attribute that ends up in documentation (eg `default` or `description`) depends on the restricted module arguments `config` or `pkgs`.
>
> Rebuild your configuration with `--show-trace` to find the offending location. Remove the references to restricted arguments (eg by escaping their antiquotations or adding a `defaultText`) or disable the sandboxed build for the failing module by setting `meta.buildDocsInSandbox = false`.
>
For full logs, run 'nix log /nix/store/0w0qq6927cvy20qmlzdr5ydsxr91f42a-lazy-options.json.drv'.
error: 1 dependencies of derivation '/nix/store/bqpgk6sy8f9kbx385hhbjpl4c3ylfnsj-options.json.drv' failed to build
error: 1 dependencies of derivation '/nix/store/rajbr0l6pky8s4rmc61g6i0fqdphkv2f-nixos-manual-html.drv' failed to build |
Could we perhaps merge the changes that are mere refactors first? Unfortunately the first commit already introduces a new feature. Just having a very basic |
If we merge this one before it solves the issue with wrapped and payload. Which would clean those changes. We can then merge only the lazy part I can open up seperate PR for only the name placeholder. I'll keep this PR. And opened up a new one to |
@infinisil I just found why omitting Seems like this line was added in #104457 I made this PR to fix the problem: |
@roberth @infinisil #354738 introduced |
5a3d22c
to
d80acd1
Compare
Eval summary
|
faeb517
to
3d399ec
Compare
3d399ec
to
d504a1e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed it together with @hsjobeki in a call, looking good, let's merge!
Cool, thank you! |
This pull request has been mentioned on NixOS Discourse. There might be relevant details there: https://discourse.nixos.org/t/use-cases-of-option-type-internals/57317/1 |
Description of changes
placeholder
="name" to configure the<name>
placeholder for the docs. This is particularly useful when"<name>"
doesn't make sense or when dealing with nested attrsOf submodule. Which would yield"<name>.<name>"
Usage example
Context
<name>
as prefix intypes.attrsOf types.submodule
is often unhelpful #295872types.attrsWith { nameType, valueType, ... }
? #193311Things done
nix.conf
? (See Nix manual)sandbox = relaxed
sandbox = true
nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD"
. Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/
)Add a 👍 reaction to pull requests you find important.