Skip to content

Document how to do conditional files/directories #723

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

Open
pydanny opened this issue Jun 3, 2016 · 18 comments · May be fixed by #2111
Open

Document how to do conditional files/directories #723

pydanny opened this issue Jun 3, 2016 · 18 comments · May be fixed by #2111
Labels
needs-docs PR Only: This PR require additional documentation

Comments

@pydanny
Copy link
Member

pydanny commented Jun 3, 2016

In the advanced_usage.rst document we should document the following techniques:

  1. By default have all the files, then use https://github.com/pydanny/cookiecutter-django/blob/master/hooks/post_gen_project.py#L163-L165 to remove those things not needed. (works everywhere)
  2. Use logic in directory names to set whether or not something is created. (Only works in Linux/OSX)
@pydanny pydanny added the needs-docs PR Only: This PR require additional documentation label Jun 3, 2016
@jamesbassett
Copy link

jamesbassett commented Jun 30, 2016

Yeah I needed this a while ago, and came up with an approach similar to 1:

  • feature flags (choice) in cookiecutter.json
  • templated manifest.yml file that lists all the files that should be removed if a feature is disabled
  • post hook to read manifest and delete relevant files

cookiecutter.json

{
    "project": "my-project",
    "feature1": [true, false],
    "feature2": [true, false],
    "feature3": [true, false]
}

manifest.yml (templated file in cookiecutter, i.e. lives beside the rest of your projects templates)

Note:

  • name matches entry in cookiecutter.json
  • enabled lowercases the value so it's valid YAML (Pythonesque True/False -> true/false)
  • resources is a list of files/folders, relative to the generated cookiecutter directory (and may contain templated variables, i.e. for package names etc)
features:
  - name: feature1
    enabled: {{cookiecutter.feature1|lower}}
    resources:
      - path/to/file/Feature1a.txt
      - path/to/file/Feature1b.txt

  - name: feature2
    enabled: {{cookiecutter.feature2|lower}}
    resources:
      - path/to/file/Feature2.txt

  - name: feature3
    enabled: {{cookiecutter.feature3|lower}}
    resources:
      - path/to/file/feature/three/folder

post_gen_project.py

#!/usr/bin/env python
import os
import shutil
import yaml

MANIFEST = "manifest.yml"


def delete_resources_for_disabled_features():
    with open(MANIFEST) as manifest_file:
        manifest = yaml.load(manifest_file)
        for feature in manifest['features']:
            if not feature['enabled']:
                print "removing resources for disabled feature {}...".format(feature['name'])
                for resource in feature['resources']:
                    delete_resource(resource)
    print "cleanup complete, removing manifest..."
    delete_resource(MANIFEST)


def delete_resource(resource):
    if os.path.isfile(resource):
        print "removing file: {}".format(resource)
        os.remove(resource)
    elif os.path.isdir(resource):
        print "removing directory: {}".format(resource)
        shutil.rmtree(resource)

if __name__ == "__main__":
    delete_resources_for_disabled_features()

@gvidon
Copy link

gvidon commented Oct 17, 2016

@jamesbassett what if you can't know absolute path to project created from template? I digged into cookiecutter source code and found that there is no way for hooks to get to know generated project fs path — https://github.com/audreyr/cookiecutter/blob/master/cookiecutter/hooks.py#L53

What did I miss?

@jamesbassett
Copy link

@gvidon the resource paths in my manifest aren't absolute - they're relative to the generated output directory (in my case they look something like src/main/kotlin/my/company/{{cookiecutter.project}}/Feature1a.txt).

As an aside, I believe cookiecutter changes directory to the output directory, so if you need to, you can access the generated directory that way from a hook as well.

@jgonggrijp
Copy link

In the OP:

  1. Use logic in directory names to set whether or not something is created. (Only works in Linux/OSX)

How does that work, and why does it not work on Windows?

@briancappello
Copy link

The solution in this thread wasn't quite clear to me, so for anybody else landing here from google:

$ tree your-cookiecutter
your-cookiecutter
├── cookiecutter.json
├── {{cookiecutter.project_slug}}
│   ├── {{cookiecutter.package_name}}
│   │   ├── file_one.py
│   │   ├── file_two.py
│   ├── README.md
├── hooks
│   └── post_gen_project.py
└── README.rst
// cat cookiecutter.json
{
  "create_readme": "y",
  "create_file_one": "y",
  "create_file_two": "n"
}
# cat post_gen_project.py
import os
import shutil

print(os.getcwd())  # prints /absolute/path/to/{{cookiecutter.project_slug}}

def remove(filepath):
    if os.path.isfile(filepath):
        os.remove(filepath)
    elif os.path.isdir(filepath):
        shutil.rmtree(filepath)

create_readme = '{{cookiecutter.create_readme}}' == 'y'
create_file_one = '{{cookiecutter.create_file_one}}' == 'y'
create_file_two = '{{cookiecutter.create_file_two}}' == 'y'

if not create_readme:
    # remove top-level file inside the generated folder
    remove('README.md')

if not create_file_one:
    # remove absolute path to file nested inside the generated folder
    remove(os.path.join(os.getcwd(), '{{cookiecutter.package_name}}', 'file_one.py'))

if not create_file_two:
    # remove relative file nested inside the generated folder
    remove(os.path.join('{{cookiecutter.package_name}}', 'file_two.py'))

@HT154
Copy link

HT154 commented Jan 2, 2019

@jgonggrijp I got curious how the conditional files work as well. After a little playing around, it's actually super simple: just use an {% if %} statement in the filename. Filenames templated as blank are omitted.

My guess is this doesn't work on Windows because % is forbidden in filenames. This could be worked around by fiddling with the Jinja block start/end strings, per http://jinja.pocoo.org/docs/2.10/api/#jinja2.Environment. Maybe a cookiecutter.json setting (something like _template_block_character, defaulting to "%") to be overridden to something like "^" for full cross-platform support.

@HT154
Copy link

HT154 commented Jan 3, 2019

After a little more playing: making entire files conditional using {% if %} in filenames works great if you're targeting *nix platforms. Using it in directory names doesn't work at all and causes errors during rendering.

@dreftymac
Copy link
Contributor

dreftymac commented Sep 28, 2019

This is just a quick note to indicate:

#723 (comment)

has broken links as of this post (2019-09-28 09:36:00).

Another option that was not enumerated in the referenced post, but is now also available:

  1. Use the cookiecutter python API, and handle conditional files and directories before running CC generate.

@ark-
Copy link

ark- commented Jun 10, 2020

After a little more playing: making entire files conditional using {% if %} in filenames works great if you're targeting *nix platforms. Using it in directory names doesn't work at all and causes errors during rendering.

Not sure the issue you're finding on Windows. At least on Windows 10 filenames with if conditionals in them do work.

For example I have a file called: {% if cookiecutter.incl_file1== 'y' %}file1.txt{% endif %} which will only be included if incl_file1 is set to y.

Hope this helps others on windows who read the above comment and assume they have to start messing around with post scripts.

@michaeljoseph
Copy link
Contributor

We can also use if-expressions

$ cat conditional/cookiecutter.json; tree conditional; cookiecutter -f --no-input conditional; tree sceletor
{
    "project_name": "sceletor",
    "generate_this": "y"
}

conditional
├── cookiecutter.json
└── {{cookiecutter.project_name}}
    └── {{\ 'test-this.txt'\ if\ cookiecutter.generate_this\ ==\ 'y'\ else\ ''\ }}

sceletor
└── test-this.txt

@oncleben31
Copy link
Contributor

  1. Use the cookiecutter python API, and handle conditional files and directories before running CC generate.

Any example somewhere @dreftymac ?

@simonw
Copy link

simonw commented Jan 27, 2021

I made some notes on this here: https://til.simonwillison.net/cookiecutter/conditionally-creating-directories

@dusktreader
Copy link

This is an old issue, but seems to be a feature that folks (including me) really need.

I've crafted this Jinja extension named cut-out-cookies adds a 'stencil' filter that can be used in filename to optionally include it. For directories, you need to use the 'stencil_path' filter along with a post-gen hook due to this issue.

The package is installable from pypi. The project includes an example structure and the README covers how to use the features.

@paul-michalik
Copy link

paul-michalik commented Jun 10, 2021

We can also use if-expressions

$ cat conditional/cookiecutter.json; tree conditional; cookiecutter -f --no-input conditional; tree sceletor
{
    "project_name": "sceletor",
    "generate_this": "y"
}

conditional
├── cookiecutter.json
└── {{cookiecutter.project_name}}
    └── {{\ 'test-this.txt'\ if\ cookiecutter.generate_this\ ==\ 'y'\ else\ ''\ }}

sceletor
└── test-this.txt

This is nice and indeed, Windows 10 supports that perfectly fine... But you'll run into issues with Git. I did not manage to commit such files into version control. Also many editors refuse to load and process files with such "odd" names. It would be much cleaner to have a convention that empty files are not added to the generated project.

@JonasKs
Copy link

JonasKs commented Oct 12, 2021

But you'll run into issues with Git.

Probably because you didn't have any files inside the directory. Empty folders are not recognized by git.

@devidw
Copy link

devidw commented Mar 12, 2022

An empty .gitkeep file could be placed in the empty directories to ensure they are committed to GIT.

@flying-sheep
Copy link

I made #2095 which should enable the ability to skip directories in the same way as skipping files.

@fotto
Copy link

fotto commented Sep 21, 2024

If you want to handle conditional files with hooks/post_gen_project.py
the following solution might be simpler than those presented above:

Say you have the following boolean switch in cookiecutter.json:

{
   ...
   "create_optional_files": "n"
   ...
}

Then in any optional file you would wrap the content in an if statement:

{%- if cookiecutter.create_optional_files == 'y' %}
file content to be rendered
{%- endif %}

When you run cookiecutter and create_optional_files is not set to y all the
optional files will be rendered as empty files.

Then the only task post_gen_project.py has to do is look for empty files and remove them:

import os

def delete_empty_files(base_dir):
    for this_dir, dirs, files in os.walk(base_dir):
        for file_name in files:
            file_path = os.path.join(base_dir, this_dir, file_name)
            file_stats = os.stat(file_path)
            if file_stats.st_size == 0:
                print(f"remove empty file {file_path}")
                os.remove(file_path)

if __name__ == "__main__":
    curr_dir = os.getcwd()
    delete_empty_files(curr_dir)   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-docs PR Only: This PR require additional documentation
Projects
None yet