Skip to content

Commit 85fc9b1

Browse files
committed
improve path resolution of path based arguments
1 parent 8c783f1 commit 85fc9b1

29 files changed

Lines changed: 1188 additions & 217 deletions

docs/plain2code_cli.md

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
# Plain2Code CLI Reference
22

33
```text
4-
usage: generate_cli.py [-h] [--verbose] [--base-folder BASE_FOLDER] [--build-folder BUILD_FOLDER] [--log-to-file | --no-log-to-file]
5-
[--log-file-name LOG_FILE_NAME] [--config-name CONFIG_NAME] [--render-range RENDER_RANGE | --render-from RENDER_FROM]
6-
[--force-render] [--unittests-script UNITTESTS_SCRIPT] [--conformance-tests-folder CONFORMANCE_TESTS_FOLDER]
7-
[--conformance-tests-script CONFORMANCE_TESTS_SCRIPT] [--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT]
8-
[--test-script-timeout TEST_SCRIPT_TIMEOUT] [--api [API]] [--api-key API_KEY] [--full-plain] [--dry-run]
9-
[--replay-with REPLAY_WITH] [--template-dir TEMPLATE_DIR] [--copy-build] [--build-dest BUILD_DEST]
10-
[--copy-conformance-tests] [--conformance-tests-dest CONFORMANCE_TESTS_DEST] [--render-machine-graph]
11-
[--logging-config-path LOGGING_CONFIG_PATH] [--headless]
4+
usage: generate_cli.py [-h] [--verbose] [--base-folder BASE_FOLDER]
5+
[--build-folder BUILD_FOLDER]
6+
[--log-to-file | --no-log-to-file]
7+
[--log-file-name LOG_FILE_NAME]
8+
[--config-name CONFIG_NAME]
9+
[--render-range RENDER_RANGE |
10+
--render-from RENDER_FROM] [--force-render]
11+
[--unittests-script UNITTESTS_SCRIPT]
12+
[--conformance-tests-folder CONFORMANCE_TESTS_FOLDER]
13+
[--conformance-tests-script CONFORMANCE_TESTS_SCRIPT]
14+
[--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT]
15+
[--test-script-timeout TEST_SCRIPT_TIMEOUT]
16+
[--api [API]] [--api-key API_KEY] [--full-plain]
17+
[--dry-run] [--replay-with REPLAY_WITH]
18+
[--template-dir TEMPLATE_DIR] [--copy-build]
19+
[--build-dest BUILD_DEST] [--copy-conformance-tests]
20+
[--conformance-tests-dest CONFORMANCE_TESTS_DEST]
21+
[--render-machine-graph]
22+
[--logging-config-path LOGGING_CONFIG_PATH]
23+
[--headless]
1224
filename
1325
14-
Render plain code to target code.
26+
Render plain code to target code. Path arguments resolve based on where they
27+
were written: values given on the command line are resolved against the
28+
current working directory, values read from config.yaml are resolved against
29+
the config file's directory, and defaults are resolved against the directory
30+
containing the plain file. Absolute paths (and paths starting with '~') are
31+
used as-is.
1532
1633
positional arguments:
17-
filename Path to the plain file to render. The directory containing this file has highest precedence for template loading, so
18-
you can place custom templates here to override the defaults. See --template-dir for more details about template
19-
loading.
34+
filename Path to the plain file to render. The directory
35+
containing this file has highest precedence for
36+
template loading, so you can place custom templates
37+
here to override the defaults. See --template-dir for
38+
more details about template loading.
2039
2140
options:
2241
-h, --help show this help message and exit
@@ -26,60 +45,89 @@ options:
2645
--build-folder BUILD_FOLDER
2746
Folder for build files
2847
--log-to-file, --no-log-to-file
29-
Enable logging to a file. Defaults to True. Set to False to disable.
48+
Enable logging to a file. Defaults to True. Set to
49+
False to disable.
3050
--log-file-name LOG_FILE_NAME
31-
Name of the log file. Defaults to 'codeplain.log'.Always resolved relative to the plain file directory.If file on
32-
this path already exists, the already existing log file will be overwritten by the current logs.
51+
Name of the log file. Defaults to 'codeplain.log'. If
52+
a file already exists at the resolved path, it will be
53+
overwritten by the current logs.
3354
--render-range RENDER_RANGE
34-
Specify a range of functionalities to render (e.g. `1` , `2`, `3`). Use comma to separate start and end IDs. If only
35-
one functionality ID is provided, only that functionality is rendered. Range is inclusive of both start and end IDs.
55+
Specify a range of functionalities to render (e.g. `1`
56+
, `2`, `3`). Use comma to separate start and end IDs.
57+
If only one functionality ID is provided, only that
58+
functionality is rendered. Range is inclusive of both
59+
start and end IDs.
3660
--render-from RENDER_FROM
37-
Continue generation starting from this specific functionality (e.g. `2`). The functionality with this ID will be
38-
included in the output. The functionality ID must match one of the functionalities in your plain file.
61+
Continue generation starting from this specific
62+
functionality (e.g. `2`). The functionality with this
63+
ID will be included in the output. The functionality
64+
ID must match one of the functionalities in your plain
65+
file.
3966
--force-render Force re-render of all the required modules.
4067
--unittests-script UNITTESTS_SCRIPT
41-
Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default:
42-
'plain_modules').
68+
Shell script to run unit tests on generated code.
69+
Receives the build folder path as its first argument
70+
(default: 'plain_modules').
4371
--conformance-tests-folder CONFORMANCE_TESTS_FOLDER
4472
Folder for conformance test files
4573
--conformance-tests-script CONFORMANCE_TESTS_SCRIPT
46-
Path to conformance tests shell script. Every conformance test script should accept two arguments: 1) Path to a
47-
folder (e.g. `plain_modules/module_name`) containing generated source code, 2) Path to a subfolder of the conformance
48-
tests folder (e.g. `conformance_tests/subfoldername`) containing test files.
74+
Path to conformance tests shell script. Every
75+
conformance test script should accept two arguments:
76+
1) Path to a folder (e.g. `plain_modules/module_name`)
77+
containing generated source code, 2) Path to a
78+
subfolder of the conformance tests folder (e.g.
79+
`conformance_tests/subfoldername`) containing test
80+
files.
4981
--prepare-environment-script PREPARE_ENVIRONMENT_SCRIPT
50-
Path to a shell script that prepares the testing environment. The script should accept the source code folder path as
51-
its first argument.
82+
Path to a shell script that prepares the testing
83+
environment. The script should accept the source code
84+
folder path as its first argument.
5285
--test-script-timeout TEST_SCRIPT_TIMEOUT
53-
Timeout for test scripts in seconds. If not provided, the default timeout of 120 seconds is used.
54-
--api [API] Alternative base URL for the API. Default: `https://api.codeplain.ai`
55-
--api-key API_KEY API key used to access the API. If not provided, the `CODEPLAIN_API_KEY` environment variable is used.
56-
--full-plain Full preview ***plain specification before code generation.Use when you want to preview context of all ***plain
57-
primitives that are going to be included in order to render the given module.
58-
--dry-run Dry run preview of the code generation (without actually making any changes).
86+
Timeout for test scripts in seconds. If not provided,
87+
the default timeout of 120 seconds is used.
88+
--api [API] Alternative base URL for the API. Default:
89+
`https://api.codeplain.ai`
90+
--api-key API_KEY API key used to access the API. If not provided, the
91+
`CODEPLAIN_API_KEY` environment variable is used.
92+
--full-plain Full preview ***plain specification before code
93+
generation.Use when you want to preview context of all
94+
***plain primitives that are going to be included in
95+
order to render the given module.
96+
--dry-run Dry run preview of the code generation (without
97+
actually making any changes).
5998
--replay-with REPLAY_WITH
6099
--template-dir TEMPLATE_DIR
61-
Path to a custom template directory. Templates are searched in the following order: 1) Directory containing the plain
62-
file, 2) Custom template directory (if provided through this argument), 3) Built-in standard_template_library
63-
directory
64-
--copy-build If set, copy the rendered contents of code in `--base-folder` folder to `--build-dest` folder after successful
65-
rendering.
100+
Path to a custom template directory. Templates are
101+
searched in the following order: 1) Directory
102+
containing the plain file, 2) Custom template
103+
directory (if provided through this argument), 3)
104+
Built-in standard_template_library directory
105+
--copy-build If set, copy the rendered contents of code in `--base-
106+
folder` folder to `--build-dest` folder after
107+
successful rendering.
66108
--build-dest BUILD_DEST
67-
Target folder to copy rendered contents of code to (used only if --copy-build is set).
109+
Target folder to copy rendered contents of code to
110+
(used only if --copy-build is set).
68111
--copy-conformance-tests
69-
If set, copy the conformance tests of code in `--conformance-tests-folder` folder to `--conformance-tests-dest`
70-
folder successful rendering. Requires --conformance-tests-script.
112+
If set, copy the conformance tests of code in
113+
`--conformance-tests-folder` folder to `--conformance-
114+
tests-dest` folder successful rendering. Requires
115+
--conformance-tests-script.
71116
--conformance-tests-dest CONFORMANCE_TESTS_DEST
72-
Target folder to copy conformance tests of code to (used only if --copy-conformance-tests is set).
117+
Target folder to copy conformance tests of code to
118+
(used only if --copy-conformance-tests is set).
73119
--render-machine-graph
74120
If set, render the state machine graph.
75121
--logging-config-path LOGGING_CONFIG_PATH
76122
Path to the logging configuration file.
77-
--headless Run in headless mode: no TUI, no terminal output except a single render-started message. All logs are written to the
78-
log file.
123+
--headless Run in headless mode: no TUI, no terminal output
124+
except a single render-started message. All logs are
125+
written to the log file.
79126
80127
configuration:
81128
--config-name CONFIG_NAME
82-
Name of the config file to look for. Looked up in the plain file directory and the current working directory.
83-
Defaults to config.yaml.
129+
Name of the config file to look for. Looked up in the
130+
plain file directory and the current working
131+
directory. Defaults to config.yaml.
84132
85133
```

file_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def get_template_directories(plain_file_path, custom_template_dir=None, default_
293293
3. Default template directory - for standard/fallback templates
294294
"""
295295
template_dirs = [
296-
os.path.dirname(plain_file_path), # Highest precedence - directory containing plain file
296+
os.path.dirname(os.path.abspath(plain_file_path)), # Highest precedence - directory containing plain file
297297
]
298298

299299
if custom_template_dir:

module_renderer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,14 @@ def render_module(self) -> None:
158158
module.wipe_module()
159159

160160
self.loaded_modules = list[PlainModule]()
161+
161162
_, rendering_failed = self._render_module(self.plain_module, self.render_range, True)
163+
162164
if not rendering_failed:
163-
# Get the last module that completed rendering
164165
if self.args.copy_build:
165166
rendered_code_path = f"{self.args.build_dest}/"
166167
else:
167-
rendered_code_path = f"{os.path.join(self.args.build_folder, self.plain_module.module_name)}/"
168+
rendered_code_path = self.args.build_folder
168169

169170
self.run_state.set_render_generated_code_path(rendered_code_path)
170171
self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path))

path_resolution.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Path resolution for CLI / config / default path arguments.
2+
3+
The rule, in one sentence: the resolution base for a relative path is determined
4+
by *where the value was written*, not by which option key it sets.
5+
6+
- Values supplied on the command line resolve against the current working
7+
directory (so they match what shell tab completion just produced).
8+
- Values read from ``config.yaml`` resolve against the directory containing
9+
that config file.
10+
- Values left at their default (not supplied anywhere) resolve against the
11+
directory containing the spec file.
12+
13+
Paths are expanded for ``~`` and returned as absolute paths. Canonicalization
14+
(symlink resolution) is left to the caller, to be done just before I/O that
15+
needs it (writing output, executing scripts) so we don't accidentally
16+
dereference links the user wanted preserved.
17+
"""
18+
19+
import os
20+
from typing import Literal, Optional
21+
22+
PathSource = Literal["cli", "config", "default"]
23+
24+
25+
def resolve_path(
26+
value: str,
27+
source: PathSource,
28+
*,
29+
cwd: str,
30+
config_dir: Optional[str] = None,
31+
spec_dir: str,
32+
) -> str:
33+
"""Resolve *value* to an absolute path using the base anchor for *source*.
34+
35+
Args:
36+
value: The path string as written by the user. May be absolute, relative,
37+
or contain a leading ``~``.
38+
source: Where the value came from -- ``"cli"``, ``"config"``, or
39+
``"default"``.
40+
cwd: Base directory for ``"cli"`` values.
41+
config_dir: Base directory for ``"config"`` values. Must be provided
42+
whenever ``source == "config"``.
43+
spec_dir: Base directory for ``"default"`` values.
44+
45+
Returns:
46+
Absolute path with ``~`` expanded. Symlinks are not resolved.
47+
"""
48+
expanded = os.path.expanduser(value)
49+
50+
if os.path.isabs(expanded):
51+
return os.path.normpath(expanded)
52+
53+
if source == "cli":
54+
base = cwd
55+
elif source == "config":
56+
if config_dir is None:
57+
raise ValueError("config_dir must be provided when source == 'config'")
58+
base = config_dir
59+
elif source == "default":
60+
base = spec_dir
61+
else:
62+
raise ValueError(f"Unknown path source: {source!r}")
63+
64+
return os.path.normpath(os.path.join(base, expanded))

plain2code.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
IndentedFormatter,
4747
LoggingHandler,
4848
dump_crash_logs,
49-
get_log_file_path,
5049
)
5150
from plain2code_state import RunState
5251
from plain2code_telemetry import capture_crash, initialize_telemetry
@@ -85,8 +84,7 @@ def setup_logging(
8584
event_bus: EventBus,
8685
run_state: RunState,
8786
log_to_file: bool,
88-
log_file_name: str,
89-
plain_file_path: Optional[str],
87+
log_file_path: str,
9088
headless: bool = False,
9189
):
9290
default_level = logging.DEBUG if args.verbose else logging.INFO
@@ -97,10 +95,9 @@ def setup_logging(
9795
logging.getLogger("transitions").setLevel(logging.ERROR)
9896
logging.getLogger("transitions.extensions.diagrams").setLevel(logging.ERROR)
9997

100-
log_file_path = get_log_file_path(plain_file_path, log_file_name)
101-
10298
# Try to load logging configuration from YAML file (takes precedence over --verbose)
10399
config_loaded = False
100+
104101
if args.logging_config_path and os.path.exists(args.logging_config_path):
105102
try:
106103
with open(args.logging_config_path, "r") as f:
@@ -123,7 +120,7 @@ def setup_logging(
123120
handler.setFormatter(formatter)
124121
root_logger.addHandler(handler)
125122

126-
if log_to_file and log_file_path:
123+
if log_to_file:
127124
try:
128125
file_handler = logging.FileHandler(log_file_path, mode="w", encoding="utf-8")
129126
file_handler.setFormatter(file_formatter)
@@ -351,7 +348,7 @@ def main(): # noqa: C901
351348
# Parse the plain file (and its required modules) once; reused by dry-run and rendering.
352349
try:
353350
plain_module = plain_modules.PlainModule(
354-
args.filename,
351+
os.path.basename(args.filename),
355352
args.build_folder,
356353
args.conformance_tests_folder,
357354
template_dirs,
@@ -378,9 +375,7 @@ def main(): # noqa: C901
378375
# Suppress Rich console output.
379376
console.quiet = True
380377

381-
default_log_level = setup_logging(
382-
args, event_bus, run_state, args.log_to_file, args.log_file_name, args.filename, args.headless
383-
)
378+
default_log_level = setup_logging(args, event_bus, run_state, args.log_to_file, args.log_file_name, args.headless)
384379

385380
initialize_telemetry()
386381

0 commit comments

Comments
 (0)