Skip to content

Commit 9667730

Browse files
authored
feat: Support STRUCTKIT_STRUCTURES_PATH environment variable (#122) (#124)
## Description This PR implements support for the `STRUCTKIT_STRUCTURES_PATH` environment variable to address #122. ## Changes - **Add environment variable support**: Users can now set `STRUCTKIT_STRUCTURES_PATH` to specify a default structures path without repeating the `-s` flag on every command - **Precedence**: Command-line arguments take precedence over environment variables (consistent with existing patterns) - **Logging**: When the environment variable is used, a log message is emitted for transparency - **Testing**: Comprehensive test suite covering all precedence scenarios - **Documentation**: Updated CLI reference with environment variables section and generate command documentation ## Implementation Details - Modified `GenerateCommand.execute()` to check for `STRUCTKIT_STRUCTURES_PATH` environment variable when `--structures-path` is not provided - Follows the existing pattern used by `STRUCTKIT_LOG_LEVEL` - All tests pass successfully ## Usage Example ```bash export STRUCTKIT_STRUCTURES_PATH=~/custom-structures structkit generate python-basic ./my-project # Equivalent to: structkit generate -s ~/custom-structures python-basic ./my-project ``` ## Testing - ✅ 5 new tests covering: - Environment variable is used when no CLI arg provided - CLI argument takes precedence over environment variable - No structures path when env var not set - Logging message is emitted - Empty env var is ignored - ✅ All existing tests continue to pass - ✅ Pre-commit hooks pass (formatting, linting) ## Closes Closes #122
1 parent e2d871b commit 9667730

File tree

3 files changed

+148
-2
lines changed

3 files changed

+148
-2
lines changed

docs/cli-reference.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ These options are available for all commands:
2121
- `-c CONFIG_FILE, --config-file CONFIG_FILE`: Path to a configuration file.
2222
- `-i LOG_FILE, --log-file LOG_FILE`: Path to a log file.
2323

24+
## Environment Variables
25+
26+
The following environment variables can be used to configure default values for CLI arguments:
27+
28+
- `STRUCTKIT_LOG_LEVEL`: Set the default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Overridden by the `--log` flag.
29+
- `STRUCTKIT_STRUCTURES_PATH`: Set the default path to structure definitions. This is used when the `--structures-path` flag is not provided.
30+
31+
**Example:**
32+
33+
```sh
34+
# Set a default structures path
35+
export STRUCTKIT_STRUCTURES_PATH=~/custom-structures
36+
37+
# Now you can omit the -s flag
38+
structkit generate python-basic ./my-project
39+
# Equivalent to: structkit generate -s ~/custom-structures python-basic ./my-project
40+
```
41+
2442
## Commands
2543

2644
### `info`
@@ -75,7 +93,7 @@ structkit generate
7593

7694
- `structure_definition` (optional): Path to the YAML configuration file (default: `.struct.yaml`).
7795
- `base_path` (optional): Base path where the structure will be created (default: `.`).
78-
- `-s STRUCTURES_PATH, --structures-path STRUCTURES_PATH`: Path to structure definitions.
96+
- `-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).
7997
- `-n INPUT_STORE, --input-store INPUT_STORE`: Path to the input store.
8098
- `-d, --dry-run`: Perform a dry run without creating any files or directories.
8199
- `--diff`: Show unified diffs for files that would be created/modified (works with `--dry-run` and in `-o console` mode).

structkit/commands/generate.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ def __init__(self, parser):
1717
structure_arg = parser.add_argument('structure_definition', nargs='?', default='.struct.yaml', type=str, help='Path to the YAML configuration file (default: .struct.yaml)')
1818
structure_arg.completer = structures_completer
1919
parser.add_argument('base_path', nargs='?', default='.', type=str, help='Base path where the structure will be created (default: current directory)')
20-
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
20+
parser.add_argument(
21+
'-s',
22+
'--structures-path',
23+
type=str,
24+
help='Path to structure definitions',
25+
default=os.getenv('STRUCTKIT_STRUCTURES_PATH')
26+
)
2127
parser.add_argument('-n', '--input-store', type=str, help='Path to the input store', default='/tmp/structkit/input.json')
2228
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
2329
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
@@ -124,6 +130,10 @@ def execute(self, args):
124130
self.logger.info(f" Structure definition: {args.structure_definition}")
125131
self.logger.info(f" Base path: {args.base_path}")
126132

133+
# Log if using STRUCTKIT_STRUCTURES_PATH environment variable
134+
if args.structures_path and os.getenv('STRUCTKIT_STRUCTURES_PATH') == args.structures_path:
135+
self.logger.info(f"Using STRUCTKIT_STRUCTURES_PATH: {args.structures_path}")
136+
127137
# Load mappings if provided
128138
mappings = {}
129139
if getattr(args, 'mappings_file', None):
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import pytest
2+
from unittest.mock import patch, MagicMock
3+
from structkit.commands.generate import GenerateCommand
4+
import argparse
5+
import os
6+
7+
8+
@pytest.fixture
9+
def parser():
10+
return argparse.ArgumentParser()
11+
12+
13+
def test_env_var_structures_path_used_when_no_cli_arg(parser):
14+
"""Test that STRUCTKIT_STRUCTURES_PATH env var is used when --structures-path is not provided."""
15+
command = GenerateCommand(parser)
16+
17+
# Re-create parser with env var set to capture it in the default
18+
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/custom/structures'}):
19+
parser_with_env = argparse.ArgumentParser()
20+
command_with_env = GenerateCommand(parser_with_env)
21+
22+
with patch('os.path.exists', return_value=True), \
23+
patch('builtins.open', new_callable=MagicMock), \
24+
patch('yaml.safe_load', return_value={'files': []}), \
25+
patch.object(command_with_env, '_create_structure') as mock_create_structure:
26+
27+
# Parse args without --structures-path
28+
args = parser_with_env.parse_args(['structure.yaml', 'base_path'])
29+
command_with_env.execute(args)
30+
31+
# Verify structures_path was set from environment variable
32+
assert args.structures_path == '/custom/structures'
33+
mock_create_structure.assert_called_once()
34+
35+
36+
def test_cli_arg_takes_precedence_over_env_var(parser):
37+
"""Test that CLI --structures-path takes precedence over STRUCTKIT_STRUCTURES_PATH env var."""
38+
command = GenerateCommand(parser)
39+
40+
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/env/structures'}), \
41+
patch('os.path.exists', return_value=True), \
42+
patch('builtins.open', new_callable=MagicMock), \
43+
patch('yaml.safe_load', return_value={'files': []}), \
44+
patch.object(command, '_create_structure') as mock_create_structure:
45+
46+
# Parse args WITH --structures-path
47+
args = parser.parse_args(['--structures-path', '/cli/structures', 'structure.yaml', 'base_path'])
48+
command.execute(args)
49+
50+
# Verify CLI arg was not overridden by env var
51+
assert args.structures_path == '/cli/structures'
52+
mock_create_structure.assert_called_once()
53+
54+
55+
def test_no_structures_path_when_env_var_not_set(parser):
56+
"""Test that structures_path remains None when neither CLI arg nor env var is provided."""
57+
# Ensure env var is not set
58+
env = os.environ.copy()
59+
env.pop('STRUCTKIT_STRUCTURES_PATH', None)
60+
61+
with patch.dict(os.environ, env, clear=True):
62+
parser_without_env = argparse.ArgumentParser()
63+
command_without_env = GenerateCommand(parser_without_env)
64+
65+
with patch('os.path.exists', return_value=True), \
66+
patch('builtins.open', new_callable=MagicMock), \
67+
patch('yaml.safe_load', return_value={'files': []}), \
68+
patch.object(command_without_env, '_create_structure') as mock_create_structure:
69+
70+
# Parse args without --structures-path
71+
args = parser_without_env.parse_args(['structure.yaml', 'base_path'])
72+
command_without_env.execute(args)
73+
74+
# Verify structures_path remains None
75+
assert args.structures_path is None
76+
mock_create_structure.assert_called_once()
77+
78+
79+
def test_env_var_logging_message(parser, caplog):
80+
"""Test that a log message is emitted when using STRUCTKIT_STRUCTURES_PATH env var."""
81+
import logging
82+
83+
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': '/custom/structures'}):
84+
parser_with_env = argparse.ArgumentParser()
85+
command_with_env = GenerateCommand(parser_with_env)
86+
87+
with patch('os.path.exists', return_value=True), \
88+
patch('builtins.open', new_callable=MagicMock), \
89+
patch('yaml.safe_load', return_value={'files': []}), \
90+
patch.object(command_with_env, '_create_structure') as mock_create_structure:
91+
92+
# Enable debug logging to capture the log message
93+
with caplog.at_level(logging.INFO):
94+
args = parser_with_env.parse_args(['structure.yaml', 'base_path'])
95+
command_with_env.execute(args)
96+
97+
# Verify log message was emitted
98+
assert 'Using STRUCTKIT_STRUCTURES_PATH: /custom/structures' in caplog.text
99+
100+
101+
def test_empty_env_var_is_ignored(parser):
102+
"""Test that an empty STRUCTKIT_STRUCTURES_PATH env var is treated as not set."""
103+
with patch.dict(os.environ, {'STRUCTKIT_STRUCTURES_PATH': ''}):
104+
parser_with_empty_env = argparse.ArgumentParser()
105+
command_with_empty_env = GenerateCommand(parser_with_empty_env)
106+
107+
with patch('os.path.exists', return_value=True), \
108+
patch('builtins.open', new_callable=MagicMock), \
109+
patch('yaml.safe_load', return_value={'files': []}), \
110+
patch.object(command_with_empty_env, '_create_structure') as mock_create_structure:
111+
112+
# Parse args without --structures-path
113+
args = parser_with_empty_env.parse_args(['structure.yaml', 'base_path'])
114+
command_with_empty_env.execute(args)
115+
116+
# Verify structures_path is empty string (from empty env var)
117+
assert args.structures_path == '' or args.structures_path is None
118+
mock_create_structure.assert_called_once()

0 commit comments

Comments
 (0)