-
-
Notifications
You must be signed in to change notification settings - Fork 182
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
introducing project mode #1022
introducing project mode #1022
Conversation
For me these tests fail on master as well |
They pass for me on |
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.
A couple of thoughts from my side:
- If you think that the
main.Worker
class does too much and it would be better to factor out the rendering methods intorenderer.Renderer
, I suggest to do this refactoring in a dedicated PR to separate concerns and ease the review. In any case, @yajo will need to decide whether he likes the refactoring. - From a template creator's perspective, I find it a bit unexpected that the files in the folder marked via
_items
are merged in the target folder. For instance, intest_render_items_list()
the files in bothsrc/apples/
andsrc/oranges/
are generated to<dst>/
and not to<dst>/apples/
and<dst>/oranges/
, respectively. I imagine there are technical reasons for this behavior, but the point of declarative project templates is to make the them readable and close to the generated project. Arguably, generating a directory structure is also in violation of this goal, but in that case the benefits outweigh the loss in readability IMO, and at least there is no implicit magic unlike the implicit merging of thesrc/apples/
andsrc/oranges/
folders. - I'm a bit confused by
test_render_items_conditional()
. The behavior tested there seems to equivalent to using a dynamic subdirectory. The only difference is the usage of the new special variable_item
. Is the goal of this test case to test the rendering of dict items? - There was some minor discussion about nested loops in Making Copier generate a complete project #908. Would nested loops be possible with the current design? If yes, could you add a test case? I am vaguely worried that the current design might not be sufficiently flexible to address more advanced cases because the rendering context is only the main context plus the current item's data, but with nested loops the context from the parent loop seems to be unavailable in a child loop.
Yeah, I thought it's easier to change from a single PR to multiple, than the other way around
The first version I did was with only one items list and without the directory. In result, all files and directories under the template root had to be named with a condition ( Those directories are already treated specially - their contents is rendered once for every item. I would find it weird if they were copied over to the output, but I guess it's just a matter of expectations of a new user of Copier vs a seasoned one. I'd rather not add it as a flag, bc removing the directories IMO would be more reasonable default, but not what older users might expect. How about those directories go to
Good catch. It's from the previous version, updated a bit thoughtlessly.
You're referring to this. I never intended to implement it due to exponential complexity of such templates. Considering the goal of this change (generating SDK or class model from a machine readable description), I'd say nested loops are out of scope. If you're generating a class model, you have a single list of table definitions or JSON schemas to generate classes for - that's a single list. In case of SDK for OpenAPI, you have a bunch of schemas and operations, that's two lists, processed in parallel. All that assuming your model is large enough to render it as multiple files, otherwise you don't even need project mode. Do you have a use case in mind? |
The tests mostly pass on an identical environment in my fork. |
@yajo Any idea how to progress with this? |
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 find surprising also what @sisp said. I think that a looped template should behave as close as possible to a non-looped one.
My expectations are that it just gets rendered in loop under different context. Since the loop is sequential, any file that gets rendered more than once would get only the last result. It is up to the template designer to avoid that kind of situations; we just have to document that.
FWIW the normal thing would be that a file that gets rendered twice results in the same output if the context didn't change or _item
isn't used within its content.
If it's needed to avoid that, then we can also add a key such as _dynamic_files
with a list of gitignore-like patterns (just like _exclude
) of files that should be rendered in loop. By default: all files. But that would be an extra thing to add later. I wouldn't include this in the MVP if possible.
So a simple design would be:
# src/copier.yaml
_items: "{{ classes }}"
classes:
type: yaml
help: write a list of classes you want to generate
default: [one, two]
user: your name
Then another file named src/{{ _item }}.txt
:
{{ user }}
I'd run copier -f src dst
and expect to get:
dst/one.txt (contents: "your name")
dst/two.txt (contents: "your name")
Do you think you would be able to implement such design?
overwrite=True, | ||
) | ||
|
||
one_rendered = (dst / "one.txt").read_text() |
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.
Why this isn't like this? 🤔
one_rendered = (dst / "one.txt").read_text() | |
one_rendered = (dst / "items" / "one.txt").read_text() |
/ "copier.yml": """ | ||
_items: items | ||
""", | ||
src | ||
/ "items" |
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.
There are too many "items" things here! 😆
Please give each a different name to let me reason easily about what are you testing here. Something like this would be enough:
/ "copier.yml": """ | |
_items: items | |
""", | |
src | |
/ "items" | |
/ "copier.yml": """ | |
_items: items_var | |
""", | |
src | |
/ "items_dir" |
@@ -0,0 +1,174 @@ | |||
from collections import ChainMap | |||
from dataclasses import dataclass, field |
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.
Probably you should change this to a pydantic dataclass, just to be consistent with the rest of Copier.
return Renderer( | ||
self.template, | ||
self.subproject, | ||
self._render_allowed, | ||
self.pretend, | ||
self.jinja_env, | ||
self.answers_relpath, | ||
ChainMap({"_item": items_value}, self._render_context), | ||
items_key, | ||
[], | ||
) |
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.
You can do this probably:
return Renderer( | |
self.template, | |
self.subproject, | |
self._render_allowed, | |
self.pretend, | |
self.jinja_env, | |
self.answers_relpath, | |
ChainMap({"_item": items_value}, self._render_context), | |
items_key, | |
[], | |
) | |
return replace( | |
self, | |
_render_context=ChainMap({"_item": items_value}, self._render_context), | |
_items_key=items_key, | |
_items_keys=[], | |
) |
return cast(SandboxedEnvironment, self.abstract_jinja_env(SandboxedEnvironment)) | ||
|
||
@cached_property | ||
def native_jinja_env(self) -> NativeEnvironment: |
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 don't see any uses of this...
[raw_items] | ||
if isinstance(raw_items, str) | ||
else cast(collections.abc.Iterable, raw_items) |
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.
If it is a string, then it should probably get rendered using the native jinja env, to allow for complex scenarios. Right?
return self._default_renderer.render_string(string) | ||
|
||
@cached_property | ||
def project_mode_keys(self) -> Iterable[str]: |
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.
All have been projects since forever... let's give it a better name:
def project_mode_keys(self) -> Iterable[str]: | |
def looped_mode_keys(self) -> Iterable[str]: |
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.
Call the file test_looped_mode.py
Oh and regarding the refactor... no problem with it. |
@yajo Have you seen my comment in the other discussion about this topic? #908 (comment) I think that approach would cover a broader range of scenarios with more intuitive API. WDYT? |
I like that design but i think it can be hard to implement. Gotta think
about this deeply.
El vie., 7 abr. 2023 11:02, Sigurd Spieckermann ***@***.***>
escribió:
… @yajo <https://github.com/yajo> Have you seen my comment in the other
discussion about this topic? #908 (comment)
<#908 (comment)>
I think that approach would cover a broader range of scenarios with more
intuitive API. WDYT?
—
Reply to this email directly, view it on GitHub
<#1022 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAHNXDPFB7OYJHTM6SSJRLDW77Q4PANCNFSM6AAAAAAVP3DKX4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Closing because the feature has been designed in #1271 and this PR doesn't follow that design. Thanks @rafalkrupinski for raising this subject! We'll wait until @0x00zer0day (or some faster volunteer) is able to publish the feature. |
Not only Worker is huge and does a lot, but the new class greatly simplifies rendering the template for each project-mode item in the next commit.
The idea behind it was that it encapsulates the template and accepts answers to render the output.
I decided to leave _allow_render in Worker and pass it as a function argument, as it uses data stored in Worker. Let me know if you can think of a better way to split responsibility between those two.
_items
incopier.yaml
as either a string or a list of strings. Internally it's always a list.{{template_root}}/{{key}}
So if you have _items: ['a'], copier expects an answer
a
to be a list of values, and uses each value to render a sub-template stored in 'a'._items
are excluded from normal rendering.example template: https://github.com/python-lapidary/lapidary-template/tree/test2