Skip to content

Fix module resolution for subdirectory paths (top and required modules)#237

Open
pedjaradenkovic wants to merge 1 commit into
mainfrom
fix/relative-module-path
Open

Fix module resolution for subdirectory paths (top and required modules)#237
pedjaradenkovic wants to merge 1 commit into
mainfrom
fix/relative-module-path

Conversation

@pedjaradenkovic

@pedjaradenkovic pedjaradenkovic commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Two related failures when a .plain module lives in / references a subdirectory:

  1. Running the CLI with a relative path containing a subdirectory fails:
    $ python plain2code.py examples/example_hello_world_python/hello_world_python.plain
    Error: Module does not exist (examples/example_hello_world_python/hello_world_python).
    
  2. A top module that requires a module in a subdirectory (requires: [dir_name/b]), rendered from outside its folder, fails when the PlainModule tree is built:
    Error: Module does not exist (b).
    

Root cause

get_template_directories always folds dirname(filename) into template_dirs[0]. But the full top-module filename was still passed to plain_file_parser, so the directory was counted twice when opening the file (open_from joins template_dirs[0] + the module name).

  • Required/imported modules are already passed as template-relative names (dir_name/b.plain), so they must keep their subdirectory prefix to resolve.
  • The top module must drop its directory prefix (it's already in template_dirs).

An earlier attempt fixed only the top module by forcing the bare stem — but that stripped the subdirectory from required modules too, breaking case 2.

Fix

  • plain2code.py: normalize the top module to os.path.basename(args.filename) at the call sites (its directory is already in template_dirs). This is the frame the parser expects.
  • plain_file.py: plain_file_parser preserves the relative subdirectory prefix in module_name (so dir_name/b resolves and is retained in build/conformance paths); absolute paths fall back to the stem.

With this, both the top module and required modules in subdirectories resolve correctly, and required-module subdirectory structure is preserved under the build folder (build/dir_name/b).

Tests

tests/test_plainfileparser.py (fixtures in tests/data/requires_subdir/):

  • test_parser_keeps_subdirectory_in_module_name — parser keeps nested/leaf for a template-relative name.
  • test_requires_module_in_subdirectory_resolves — builds the full PlainModule tree with a required subdirectory module (the path that broke) and asserts module names + build folder (build/nested/leaf).

Verification

  • Full suite: 217 passed; black / flake8 / mypy clean.
  • Original relative-subfolder CLI invocation works.
  • requires: [dir_name/b] from outside the folder now builds the module tree and reaches the API instead of failing at "Module does not exist".

@pedjaradenkovic pedjaradenkovic changed the title Fix module resolution for relative subdirectory paths Fix module resolution for subdirectory paths (top and required modules) Jul 8, 2026
The top module's directory is already folded into template_dirs by
get_template_directories, so passing the full filename to plain_file_parser
double-counted the directory (relative subdirectory paths failed with
'Module does not exist'). Normalize the top module to its basename at the
call sites in plain2code.py; plain_file_parser keeps preserving relative
subdirectory prefixes so required/imported modules (e.g. 'dir_name/b') resolve.

Add PlainModule-tree and parser-level regression tests.
@pedjaradenkovic pedjaradenkovic force-pushed the fix/relative-module-path branch from 92cb325 to 7722cea Compare July 8, 2026 13:41
@NejcS NejcS self-requested a review July 14, 2026 05:51
@NejcS

NejcS commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

I don't think this is needed anymore since PR #168 was merged. I tested on the latest main branch (after #168 was merged) and I can run the rendering the way that is described in the two above failures.

Case 1:

(.venv) ➜  codeplain git:(main) pwd
/Users/nejcstebe/codeplain/codeplain
(.venv) ➜  codeplain git:(main) python plain2code.py examples/example_hello_world_python/hello_world_python.plain --full-plain
Full plain text:

---
description: '"hello, world" in Plain (Python version)'
import:
  - python-console-app-template
---

***implementation reqs***

- :MainExecutableFile: of :App: should be called "hello_world.py".

***functional specs***

- Display "hello, world"

  ***acceptance tests***

  - :App: should exit with status code 0 indicating successful execution.

  - :App: should complete execution in under 1 second.

Case 2, I modified the password manager example so that vault-cli.plain requires temp_dir/password_manager.plain.

(.venv) ➜  codeplain git:(main) ✗ pwd
/Users/nejcstebe/codeplain/codeplain
(.venv) ➜  codeplain git:(main) ✗ python plain2code.py ../cli-password-manager/vault-cli.plain --full-plain
Full plain text:

---
import:
  - vault_reqs
  - entry_reqs
requires:
  - test_dir/password_manager
---

***implementation reqs***

- The commands should be implemented in the :CLI:.

- :CLI: is defined in the vault.py

- The functions from :PasswordManagerModule: should be used for password inputs.

- All :CLI: functions operating on :Vault: should accept optional --path argument that explicitly defines :Vault: storage file path.

***test reqs***

- Some functionalities require user input to work. The conformance tests should simulate or mock the input functions to test these functionalities.

***functional specs***

- Implement the Help command which prints out the usage for the :Vault: app
  - This Help command should be called if :CLI: is run without input arguments or help command argument.

- Implement an Initialization Command (`init`)
  - Ask the user for the password
  - Get the :EncryptionKey: and KDF parameters using the get_key function in the key_management module.
  - The function should initialize the :Vault: using the :EncryptionKey: and KDF parameters.

- Implement an Add Command (`add`) that adds an additional password to the :Vault:
  - Ask the user for entry details
  - The function should call the add_entry function of the vault_functions module.

- Implement the Get Command (`get`) which retrieves the :Entry: password
  - Takes an :Entry: ID and the :Vault: storage file path as input.
  - The function should call the get_entry function of the vault_functions module.

- Implement the Delete Command (`delete`)
  - Takes an :Entry: ID and the :Vault: storage file path as input.
  - The function should call the delete_entry function of the vault_functions module.

- Implement the List Command (`list`)
  - Takes the :Vault: storage file path as input.
  - The function should call the list_entries function of the vault_functions module.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants