Skip to content

Commit

Permalink
Fixed tests and overridden functions !minor
Browse files Browse the repository at this point in the history
  • Loading branch information
coordt committed Jul 24, 2023
1 parent ff695e3 commit 6fb9404
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 76 deletions.
122 changes: 88 additions & 34 deletions cookie_composer/cc_overrides.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"""This overrides the default cookie cutter environment."""
from typing import Any, List

import json
from typing import Any, List, MutableMapping

from cookiecutter.environment import StrictEnvironment
from cookiecutter.exceptions import UndefinedVariableInTemplate
from cookiecutter.prompt import (
prompt_choice_for_config,
read_user_dict,
read_user_variable,
read_user_yes_no,
render_variable,
)
from jinja2 import UndefinedError
from jinja2 import Environment, UndefinedError
from jinja2.ext import Extension

from cookie_composer.data_merge import Context
Expand All @@ -28,11 +28,11 @@ def jsonify_context(value: Any) -> dict:
class JsonifyContextExtension(Extension):
"""Jinja2 extension to convert a Python object to JSON."""

def __init__(self, environment):
def __init__(self, environment: Environment):
"""Initialize the extension with the given environment."""
super().__init__(environment)

def jsonify(obj):
def jsonify(obj: Any) -> str:
return json.dumps(obj, sort_keys=True, indent=4, default=jsonify_context)

environment.filters["jsonify"] = jsonify
Expand All @@ -53,7 +53,7 @@ def __init__(self, **kwargs):
del self.extensions["cookiecutter.extensions.JsonifyExtension"]
self.add_extension("cookie_composer.cc_overrides.JsonifyContextExtension")

def _read_extensions(self, context) -> List[str]:
def _read_extensions(self, context: MutableMapping[str, Any]) -> List[str]:
"""
Return list of extensions as str to be passed on to the Jinja2 env.
Expand All @@ -69,7 +69,7 @@ def _read_extensions(self, context) -> List[str]:
return [str(ext) for ext in context.get("_extensions", [])]


def prompt_for_config(prompts: dict, existing_config: Context, no_input=False) -> Context:
def prompt_for_config(prompts: dict, existing_config: Context, no_input: bool = False) -> Context:
"""
Prompt user to enter a new config using an existing config as a basis.
Expand All @@ -88,7 +88,8 @@ def prompt_for_config(prompts: dict, existing_config: Context, no_input=False) -
Returns:
A new configuration context
"""
import copy
if "cookiecutter" in prompts:
prompts = prompts["cookiecutter"]

# Make sure we have a fresh layer to populate
if existing_config.is_empty:
Expand All @@ -98,9 +99,77 @@ def prompt_for_config(prompts: dict, existing_config: Context, no_input=False) -

env = CustomStrictEnvironment(context=existing_config)

# First pass: Handle simple and raw variables, plus choices.
# These must be done first because the dictionaries keys and
# values might refer to them.
context_prompts = {}
if "__prompts__" in prompts:
context_prompts = prompts["__prompts__"]
del prompts["__prompts__"]

_render_simple(context, context_prompts, env, no_input, prompts)

_render_dicts(context, env, no_input, prompts)

return context


def _render_dicts(context: Context, env: Environment, no_input: bool, prompts: dict) -> None:
"""
Render dictionaries.
This is the second pass of rendering. It must be done after the first pass because that renders
the values that might be used as keys or values in the dictionaries.
I hate that this uses a side effect to modify the context, but I don't see a better way.
Args:
context: The current context
env: The current environment for rendering
no_input: Should we prompt the user for input?
prompts: The default prompts for the context
Raises:
UndefinedVariableInTemplate: If a variable in a prompt defaults is not in the context
"""
# Second pass; handle the dictionaries.
for key, raw in prompts.items():
# Skip private type dicts not ot be rendered.
if key.startswith("_") and not key.startswith("__"):
continue

try:
if isinstance(raw, dict):
# We are dealing with a dict variable
val = render_variable(env, raw, context.flatten())

if not no_input and not key.startswith("__"):
val = read_user_dict(key, val, prompts)

context[key] = val
except UndefinedError as err:
msg = f"Unable to render variable '{key}'"
raise UndefinedVariableInTemplate(msg, err, context) from err


def _render_simple(context: Context, context_prompts: dict, env: Environment, no_input: bool, prompts: dict) -> None:
"""
Render simple variables, raw variables, and choices.
This is the first pass. It must be done first because the dictionary's keys and
values might refer to them.
I hate that this uses a side effect to modify the context, but I don't see a better way.
Args:
context: The current context
context_prompts: The human prompts for the context
env: The current environment for rendering
no_input: Should we prompt the user for input?
prompts: The default prompts for the context
Raises:
UndefinedVariableInTemplate: If a variable in a prompt defaults is not in the context
"""
import copy

for key, raw in prompts.items():
if key.startswith("_") and not key.startswith("__"):
context[key] = raw
Expand All @@ -115,37 +184,22 @@ def prompt_for_config(prompts: dict, existing_config: Context, no_input=False) -
try:
if isinstance(raw, list):
# We are dealing with a choice variable
val = prompt_choice_for_config(context, env, key, raw, no_input)
val = prompt_choice_for_config(context, env, key, raw, no_input, context_prompts)
context[key] = val
elif isinstance(raw, bool):
# We are dealing with a boolean variable
if no_input:
context[key] = render_variable(env, raw, context)
else:
context[key] = read_user_yes_no(key, raw, context_prompts)
elif not isinstance(raw, dict):
# We are dealing with a regular variable
val = render_variable(env, raw, context)

if not no_input:
val = read_user_variable(key, val)
val = read_user_variable(key, val, context_prompts)

context[key] = val
except UndefinedError as err:
msg = f"Unable to render variable '{key}'"
raise UndefinedVariableInTemplate(msg, err, context) from err

# Second pass; handle the dictionaries.
for key, raw in prompts.items():
# Skip private type dicts not ot be rendered.
if key.startswith("_") and not key.startswith("__"):
continue

try:
if isinstance(raw, dict):
# We are dealing with a dict variable
val = render_variable(env, raw, context)

if not no_input and not key.startswith("__"):
val = read_user_dict(key, val)

context[key] = val
except UndefinedError as err:
msg = f"Unable to render variable '{key}'"
raise UndefinedVariableInTemplate(msg, err, context) from err

return context
2 changes: 1 addition & 1 deletion cookie_composer/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class RenderedLayer(BaseModel):
location: DirectoryPath
"""The directory where the layer was rendered."""

new_context: Dict[str, Any]
new_context: MutableMapping[str, Any]
"""The context based on questions asked."""

latest_commit: Optional[str] = None
Expand Down
28 changes: 23 additions & 5 deletions cookie_composer/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import click
from cookiecutter.config import get_user_config
from cookiecutter.generate import generate_context, generate_files
from cookiecutter.main import _patch_import_path_for_repo
from cookiecutter.repository import determine_repo_dir
from cookiecutter.utils import rmtree

Expand Down Expand Up @@ -123,8 +124,11 @@ def render_layer(
repo = get_repo(repo_dir)
repo.git.checkout(layer_config.commit)

context = get_layer_context(layer_config, repo_dir, user_config, full_context)
print(context)
_patch_import_path_for_repo(repo_dir)
os.path.basename(os.path.abspath(repo_dir))
Path(repo_dir) / "cookiecutter.json"

context = get_layer_context(layer_config, Path(repo_dir), user_config, full_context)
if accept_hooks == "ask":
_accept_hooks = click.confirm("Do you want to execute hooks?")
else:
Expand Down Expand Up @@ -155,7 +159,7 @@ def render_layer(


def get_layer_context(
layer_config: LayerConfig, repo_dir: str, user_config: dict, full_context: Optional[Context] = None
layer_config: LayerConfig, repo_dir: Path, user_config: dict, full_context: Optional[Context] = None
) -> Context:
"""
Get the context for a layer pre-rendering values using previous layers contexts as defaults.
Expand All @@ -170,6 +174,9 @@ def get_layer_context(
The context for rendering the layer
"""
full_context = full_context or Context()
import_patch = _patch_import_path_for_repo(str(repo_dir))
# template_name = repo_dir.stem
context_file = Path(repo_dir) / "cookiecutter.json"

# _copy_without_render is template-specific and fails if overridden,
# So we are going to remove it from the "defaults" when generating the context
Expand All @@ -178,12 +185,23 @@ def get_layer_context(
# This pulls in the template context and overrides the values with the user config defaults
# and the defaults specified in the layer.
context = generate_context(
context_file=Path(repo_dir) / "cookiecutter.json",
context_file=context_file,
default_context=user_config["default_context"],
extra_context=layer_config.context or {},
)
context_for_prompting = context

with import_patch:
if context_for_prompting["cookiecutter"]:
context["cookiecutter"].update(
prompt_for_config(context_for_prompting, full_context, layer_config.no_input)
)
if "template" in context["cookiecutter"]:
# TODO: decide how to deal with nested configuration files.
# For now, we are just going to ignore them.
pass
full_context.update(context["cookiecutter"])
return prompt_for_config(full_context, layer_config.no_input)
return full_context


def render_layers(
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ typing-modules = ["typing", "types", "typing_extensions", "mypy", "mypy_extensio

[tool.ruff.per-file-ignores]
"tests/*"=["S101", "PLR0913", "PLR0915", "PGH003", "ANN001", "ANN202", "ANN201", "PLR0912", "TRY301", "PLW0603", "PLR2004", "ANN101", "S106", "TRY201", "ANN003", "ANN002", "S105", "TRY003"]
"cookie_composer/cc_overrides.py"=["C901"]

[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
@pytest.fixture
def fixtures_path() -> Path:
"""Return the path to the testing fixtures."""

return Path(__file__).parent / "fixtures"


Expand Down
4 changes: 1 addition & 3 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Authentication tests."""
from typing import Optional

import functools
import json
import os
from typing import Optional

from cookie_composer import authentication

Expand Down
Loading

0 comments on commit 6fb9404

Please sign in to comment.