Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ These options are available for all commands:
- `-c CONFIG_FILE, --config-file CONFIG_FILE`: Path to a configuration file.
- `-i LOG_FILE, --log-file LOG_FILE`: Path to a log file.

## Environment Variables

The following environment variables can be used to configure default values for CLI arguments:

- `STRUCTKIT_LOG_LEVEL`: Set the default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Overridden by the `--log` flag.
- `STRUCTKIT_STRUCTURES_PATH`: Set the default path to structure definitions. This is used when the `--structures-path` flag is not provided.

**Example:**

```sh
# Set a default structures path
export STRUCTKIT_STRUCTURES_PATH=~/custom-structures

# Now you can omit the -s flag
structkit generate python-basic ./my-project
# Equivalent to: structkit generate -s ~/custom-structures python-basic ./my-project
```

## Commands

### `info`
Expand Down Expand Up @@ -75,7 +93,7 @@ structkit generate

- `structure_definition` (optional): Path to the YAML configuration file (default: `.struct.yaml`).
- `base_path` (optional): Base path where the structure will be created (default: `.`).
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions.
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions. Can also be set via the `STRUCTKIT_STRUCTURES_PATH` environment variable (CLI flag takes precedence).
- `-n INPUT_STORE, --input-store INPUT_STORE`: Path to the input store.
- `-d, --dry-run`: Perform a dry run without creating any files or directories.
- `--diff`: Show unified diffs for files that would be created/modified (works with `--dry-run` and in `-o console` mode).
Expand Down
12 changes: 11 additions & 1 deletion structkit/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ def __init__(self, parser):
structure_arg = parser.add_argument('structure_definition', nargs='?', default='.struct.yaml', type=str, help='Path to the YAML configuration file (default: .struct.yaml)')
structure_arg.completer = structures_completer
parser.add_argument('base_path', nargs='?', default='.', type=str, help='Base path where the structure will be created (default: current directory)')
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
parser.add_argument(
'-s',
'--structures-path',
type=str,
help='Path to structure definitions',
default=os.getenv('STRUCTKIT_STRUCTURES_PATH')
)
parser.add_argument('-n', '--input-store', type=str, help='Path to the input store', default='/tmp/structkit/input.json')
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
Expand Down Expand Up @@ -124,6 +130,10 @@ def execute(self, args):
self.logger.info(f" Structure definition: {args.structure_definition}")
self.logger.info(f" Base path: {args.base_path}")

# Log if using STRUCTKIT_STRUCTURES_PATH environment variable
if args.structures_path and os.getenv('STRUCTKIT_STRUCTURES_PATH') == args.structures_path:
self.logger.info(f"Using STRUCTKIT_STRUCTURES_PATH: {args.structures_path}")

# Load mappings if provided
mappings = {}
if getattr(args, 'mappings_file', None):
Expand Down
118 changes: 118 additions & 0 deletions tests/test_env_var_structures_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import pytest
from unittest.mock import patch, MagicMock
from structkit.commands.generate import GenerateCommand
import argparse
import os


@pytest.fixture
def parser():
return argparse.ArgumentParser()


def test_env_var_structures_path_used_when_no_cli_arg(parser):
"""Test that STRUCTKIT_STRUCTURES_PATH env var is used when --structures-path is not provided."""
command = GenerateCommand(parser)

# Re-create parser with env var set to capture it in the default
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/custom/structures'}):
parser_with_env = argparse.ArgumentParser()
command_with_env = GenerateCommand(parser_with_env)

with patch('os.path.exists', return_value=True), \
patch('builtins.open', new_callable=MagicMock), \
patch('yaml.safe_load', return_value={'files': []}), \
patch.object(command_with_env, '_create_structure') as mock_create_structure:

# Parse args without --structures-path
args = parser_with_env.parse_args(['structure.yaml', 'base_path'])
command_with_env.execute(args)

# Verify structures_path was set from environment variable
assert args.structures_path == '/custom/structures'
mock_create_structure.assert_called_once()


def test_cli_arg_takes_precedence_over_env_var(parser):
"""Test that CLI --structures-path takes precedence over STRUCTKIT_STRUCTURES_PATH env var."""
command = GenerateCommand(parser)

with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/env/structures'}), \
patch('os.path.exists', return_value=True), \
patch('builtins.open', new_callable=MagicMock), \
patch('yaml.safe_load', return_value={'files': []}), \
patch.object(command, '_create_structure') as mock_create_structure:

# Parse args WITH --structures-path
args = parser.parse_args(['--structures-path', '/cli/structures', 'structure.yaml', 'base_path'])
command.execute(args)

# Verify CLI arg was not overridden by env var
assert args.structures_path == '/cli/structures'
mock_create_structure.assert_called_once()


def test_no_structures_path_when_env_var_not_set(parser):
"""Test that structures_path remains None when neither CLI arg nor env var is provided."""
# Ensure env var is not set
env = os.environ.copy()
env.pop('STRUCTKIT_STRUCTURES_PATH', None)

with patch.dict(os.environ, env, clear=True):
parser_without_env = argparse.ArgumentParser()
command_without_env = GenerateCommand(parser_without_env)

with patch('os.path.exists', return_value=True), \
patch('builtins.open', new_callable=MagicMock), \
patch('yaml.safe_load', return_value={'files': []}), \
patch.object(command_without_env, '_create_structure') as mock_create_structure:

# Parse args without --structures-path
args = parser_without_env.parse_args(['structure.yaml', 'base_path'])
command_without_env.execute(args)

# Verify structures_path remains None
assert args.structures_path is None
mock_create_structure.assert_called_once()


def test_env_var_logging_message(parser, caplog):
"""Test that a log message is emitted when using STRUCTKIT_STRUCTURES_PATH env var."""
import logging

with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/custom/structures'}):
parser_with_env = argparse.ArgumentParser()
command_with_env = GenerateCommand(parser_with_env)

with patch('os.path.exists', return_value=True), \
patch('builtins.open', new_callable=MagicMock), \
patch('yaml.safe_load', return_value={'files': []}), \
patch.object(command_with_env, '_create_structure') as mock_create_structure:

# Enable debug logging to capture the log message
with caplog.at_level(logging.INFO):
args = parser_with_env.parse_args(['structure.yaml', 'base_path'])
command_with_env.execute(args)

# Verify log message was emitted
assert 'Using STRUCTKIT_STRUCTURES_PATH: /custom/structures' in caplog.text


def test_empty_env_var_is_ignored(parser):
"""Test that an empty STRUCTKIT_STRUCTURES_PATH env var is treated as not set."""
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': ''}):
parser_with_empty_env = argparse.ArgumentParser()
command_with_empty_env = GenerateCommand(parser_with_empty_env)

with patch('os.path.exists', return_value=True), \
patch('builtins.open', new_callable=MagicMock), \
patch('yaml.safe_load', return_value={'files': []}), \
patch.object(command_with_empty_env, '_create_structure') as mock_create_structure:

# Parse args without --structures-path
args = parser_with_empty_env.parse_args(['structure.yaml', 'base_path'])
command_with_empty_env.execute(args)

# Verify structures_path is empty string (from empty env var)
assert args.structures_path == '' or args.structures_path is None
mock_create_structure.assert_called_once()
Loading