diff --git a/.gitea/workflows/embed.yaml b/.gitea/workflows/embed.yaml index 8530b55..bdc54dc 100644 --- a/.gitea/workflows/embed.yaml +++ b/.gitea/workflows/embed.yaml @@ -34,4 +34,4 @@ jobs: VECTORDB_TOKEN: ${{ secrets.VECTORDB_TOKEN }} run: | cd VectorLoader - python -m src.run --full + python -m src.run diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c29706..5dc5c07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,6 +67,9 @@ "xmemory": "cpp", "xstring": "cpp", "xtr1common": "cpp", - "xutility": "cpp" + "xutility": "cpp", + "fstream": "cpp", + "iostream": "cpp", + "codecvt": "cpp" } } \ No newline at end of file diff --git a/README.md b/README.md index d8f46c7..541ba6a 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,26 @@ # prodir -This is a Python package designed to display directory structures in a tree-like format. This tool provides an easy-to-read overview of the directory hierarchy, making it easier for developers to navigate complex project structures. +This is a Python package designed to display and create directory structures in a tree-like format. This tool provides an easy-to-read overview of the directory hierarchy, making it easier for developers to navigate complex project structures and automate the creation of directory layouts from predefined files. #### Installation -To install `tree_structurer`, you can use pip: +To install `prodir`, you can use pip: ```bash - pip install git+https://gitea.fabelous.app/Fabel/prodir.git - +pip install git+https://gitea.fabelous.app/Fabel/prodir.git ``` #### Usage -You can run `tree_structurer` from the command line. By default, it will analyze the current directory and print its structure in a tree-like format. You can also specify a path to another directory. +You can run `prodir` from the command line. By default, it will analyze the current directory and print its structure in a tree-like format. You can also specify a path to another directory. To display the directory structure of the current directory: ```bash -python -m prodir +python -m prodir display ``` -or +or ```bash prodir @@ -29,18 +28,38 @@ prodir To display the directory structure of a specific directory: +```bash +python -m prodir display /path/to/directory +``` +or + + ```bash python -m prodir /path/to/directory ``` +To create a directory structure from a file: + +```bash +python -m prodir create /path/to/structure/file.txt -o /output/directory + +or + +``` + +```bash +python -m prodir create /path/to/structure/file.txt #will create it in current dir +``` + #### Options - `-v` or `--verbose`: Show more detailed output, including information about the path being analyzed. +- `-o` or `--output`: Specify the output directory for the `create` command. -Example: +Examples: ```bash -python -m prodir /path/to/directory -v +python -m prodir display /path/to/directory -v ``` #### Important Files and Folders Ignored @@ -49,16 +68,46 @@ By default, `prodir` ignores files and folders that start with `_` or `.`. Howev - `__init__.py` and `__main__.py` files: These are considered important and will be included in the output. - Special folders like `build`, `.git`, `node_modules`, etc.: These are also ignored to keep the output focused on the essential parts of the directory structure. +#### Example Input + +```plaintext +project/ +├── src/ +│ ├── __init__.py +│ ├── main.py +│ ├── module1.py +│ └── module2.py +├── config/ +│ └── config.yaml +├── .gitignore +├── pyproject.toml +├── setup.py +├── LICENSE +└── README.m +``` #### Example Output -``` -Analyzing directory: /path/to/directory: +```plaintext +LICENSE +README.m +config/ +└── config.yaml +pyproject.toml +setup.py +src/ +├── __init__.py ├── main.py -├── module1 -│ ├── __init__.py -│ └── submodule1.py -└── utils - ├── helper.py - └── constants.py +├── module1.py +└── module2.py ``` + +#### Commands + +- `display`: Displays the directory structure of a specified path or the current directory. + - Usage: `prodir display [path] [-v]` + - Example: `python -m prodir display /path/to/directory` + +- `create`: Creates a directory structure from a file containing the structure definition. + - Usage: `prodir create [file] [output_directory] [-v]` + - Example: `python -m prodir create /path/to/structure/file.txt` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6a57252..f0b5efd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "prodir" -version = "0.0.6" -description = "A module for analyzing directory structures" +version = "0.1.0" +description = "A module for analyzing and creating directory structures" scripts = {prodir = "prodir.__main__:main"} dependencies = [] diff --git a/setup.py b/setup.py index 79f7bc5..baf9d09 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ tree_structurer_module = Extension( setup( name='prodir', - version='0.0.6', + version='0.1.0', description='A module for analyzing directory structures', ext_modules=[tree_structurer_module], packages=find_packages(where="src"), diff --git a/src/prodir/__init__.py b/src/prodir/__init__.py index 520a304..84442b3 100644 --- a/src/prodir/__init__.py +++ b/src/prodir/__init__.py @@ -1 +1,11 @@ -from prodir._tree_structurer import create_tree_structurer, get_structure \ No newline at end of file +from prodir._tree_structurer import ( + create_tree_structurer, + get_structure, + create_structure_from_file +) + +__all__ = [ + 'create_tree_structurer', + 'get_structure', + 'create_structure_from_file' +] \ No newline at end of file diff --git a/src/prodir/__main__.py b/src/prodir/__main__.py index 1967b82..f6beed9 100644 --- a/src/prodir/__main__.py +++ b/src/prodir/__main__.py @@ -1,36 +1,118 @@ import os import sys import argparse -from prodir import create_tree_structurer, get_structure +from prodir import ( + create_tree_structurer, + get_structure, + create_structure_from_file +) def main(): - parser = argparse.ArgumentParser(description='Display directory structure in a tree-like format') - parser.add_argument('path', nargs='?', default=os.getcwd(), - help='Path to analyze (default: current directory)') - parser.add_argument('-v', '--verbose', action='store_true', - help='Show more detailed output') + # Create the main parser + parser = argparse.ArgumentParser( + prog='prodir', # Set program name to prodir + description='Directory structure tool: Display and create directory structures' + ) + + # Create subparsers for different commands + subparsers = parser.add_subparsers(dest='command', help='Available commands') + + # Display command + display_parser = subparsers.add_parser('display', + prog='prodir display', # Set display command name + help='Display directory structure') + display_parser.add_argument( + 'path', + nargs='?', + default=os.getcwd(), + help='Path to analyze (default: current directory)' + ) + display_parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show more detailed output' + ) + + # Create command + create_parser = subparsers.add_parser('create', + prog='prodir create', # Set create command name + help='Create directory structure') + create_parser.add_argument( + 'file', + help='File containing the directory structure' + ) + create_parser.add_argument( + '-o', '--output', + default=os.getcwd(), + help='Output directory (default: current directory)' + ) + create_parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show more detailed output' + ) + + # Check if a direct path was provided + if len(sys.argv) > 1 and not sys.argv[1].startswith('-') and not sys.argv[1] in ['display', 'create']: + # Convert to display command with path + sys.argv.insert(1, 'display') + args = parser.parse_args() - + + # If no command is specified, use display with current directory + if not args.command: + args.command = 'display' + args.path = os.getcwd() + args.verbose = False + try: - if args.verbose: - print(f"Analyzing directory: {args.path}") - create_tree_structurer() - structure = get_structure(args.path) + + if args.command == 'display': + if args.verbose: + print(f"Analyzing directory: {args.path}") + try: + structure = get_structure(args.path) + for line in structure: + print(line) + except FileNotFoundError: + print("Error: Directory does not exist", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(1) + + elif args.command == 'create': + if args.verbose: + print(f"Creating directory structure in: {args.output}") + print(f"Using structure from file: {args.file}") + + # Check if the output path exists + if not os.path.exists(args.output): + print(f"Error: The specified output path '{args.output}' does not exist.", file=sys.stderr) + exit(1) + + try: + create_structure_from_file(args.file, args.output) + if args.verbose: + print("Structure created successfully") + print("\nResulting structure:") + structure = get_structure(args.output) + for line in structure: + print(line) + except FileNotFoundError as e: + if 'structure file' in str(e): + print("Error: Unable to open structure file", file=sys.stderr) + else: + print("Error: Directory does not exist", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(1) - for line in structure: - print(line) - except Exception as e: - error_msg = str(e) - if "Directory does not exist" in error_msg: - print(f"Error: The specified directory does not exist: {args.path}", file=sys.stderr) - elif "Directory is empty" in error_msg: - print(f"Error: The specified directory is empty: {args.path}", file=sys.stderr) - elif "Path is not a directory" in error_msg: - print(f"Error: The specified path is not a directory: {args.path}", file=sys.stderr) - else: - print(f"Error: {error_msg}", file=sys.stderr) + print(f"Error: {str(e)}", file=sys.stderr) sys.exit(1) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/prodir/cpp/bindings.cpp b/src/prodir/cpp/bindings.cpp index 6aa031a..affbede 100644 --- a/src/prodir/cpp/bindings.cpp +++ b/src/prodir/cpp/bindings.cpp @@ -18,19 +18,26 @@ static PyObject* get_structure(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, "|s", &path)) { return NULL; } - if (g_tree_structurer == nullptr) { PyErr_SetString(TreeStructurerError, "TreeStructurer not initialized"); return NULL; } - + try { std::string path_str = path ? path : ""; std::vector structure = g_tree_structurer->get_directory_structure(path_str); PyObject* list = PyList_New(structure.size()); + if (!list) { + return NULL; + } for (size_t i = 0; i < structure.size(); i++) { - PyList_SET_ITEM(list, i, PyUnicode_FromString(structure[i].c_str())); - } + PyObject* str = PyUnicode_FromString(structure[i].c_str()); + if (!str) { + Py_DECREF(list); + return NULL; + } + PyList_SET_ITEM(list, i, str); + } return list; } catch (const std::exception& e) { PyErr_SetString(TreeStructurerError, e.what()); @@ -38,27 +45,50 @@ static PyObject* get_structure(PyObject* self, PyObject* args) { } } +static PyObject* create_structure_from_file(PyObject* self, PyObject* args) { + const char* structure_file = nullptr; + const char* target_path = "."; // Default to current directory + if (!PyArg_ParseTuple(args, "s|s", &structure_file, &target_path)) { + return NULL; + } + + if (g_tree_structurer == nullptr) { + PyErr_SetString(TreeStructurerError, "TreeStructurer not initialized"); + return NULL; + } + + try { + g_tree_structurer->create_structure_from_file(structure_file, target_path); + Py_RETURN_NONE; + } catch (const std::exception& e) { + PyErr_SetString(TreeStructurerError, e.what()); + return NULL; + } +} + static PyMethodDef TreeStructurerMethods[] = { - {"create_tree_structurer", create_tree_structurer, METH_NOARGS, + {"create_tree_structurer", create_tree_structurer, METH_NOARGS, "Create a new TreeStructurer instance"}, {"get_structure", get_structure, METH_VARARGS, "Get the directory structure for the given path"}, + {"create_structure_from_file", create_structure_from_file, METH_VARARGS, + "Create directory structure from a file containing the structure description"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef tree_structurer_module = { PyModuleDef_HEAD_INIT, - "_tree_structurer", // Changed module name to match Python import - "Module for analyzing directory structures", + "_tree_structurer", + "Module for analyzing and creating directory structures", -1, TreeStructurerMethods }; -PyMODINIT_FUNC PyInit__tree_structurer(void) { // Changed function name to match module name +PyMODINIT_FUNC PyInit__tree_structurer(void) { PyObject* m = PyModule_Create(&tree_structurer_module); if (m == NULL) return NULL; - + TreeStructurerError = PyErr_NewException("tree_structurer.error", NULL, NULL); Py_XINCREF(TreeStructurerError); if (PyModule_AddObject(m, "error", TreeStructurerError) < 0) { @@ -67,6 +97,6 @@ PyMODINIT_FUNC PyInit__tree_structurer(void) { // Changed function name to matc Py_DECREF(m); return NULL; } - + return m; } \ No newline at end of file diff --git a/src/prodir/cpp/tree_structurer.cpp b/src/prodir/cpp/tree_structurer.cpp index 2ef3be2..2302cce 100644 --- a/src/prodir/cpp/tree_structurer.cpp +++ b/src/prodir/cpp/tree_structurer.cpp @@ -1,6 +1,12 @@ #include "tree_structurer.hpp" + #include #include +#include +#include +#include +#include +#include namespace fs = std::filesystem; @@ -10,9 +16,9 @@ bool TreeStructurer::should_ignore_dir(const std::string& dirname) { ".git", ".idea", ".vscode", "__pycache__", "**pycache**" }; - // Check for __main__.py or __init__.py files + // Allow these Python files even if their names start with underscores. if (!dirname.empty() && (dirname == "__main__.py" || dirname == "__init__.py")) { - return false; // Do not ignore these files + return false; } if (std::find(ignore_list.begin(), ignore_list.end(), dirname) != ignore_list.end()) { @@ -37,9 +43,8 @@ bool TreeStructurer::should_ignore_file(const std::string& filename) { ".o", ".obj", ".a", ".lib" }; - // Check for __main__.py or __init__.py files if (!filename.empty() && (filename == "__main__.py" || filename == "__init__.py")) { - return false; // Do not ignore these files + return false; } if (!filename.empty() && (filename[0] == '.' || filename[0] == '_')) { @@ -55,12 +60,12 @@ bool TreeStructurer::should_ignore_file(const std::string& filename) { return false; } -// Add the missing get_relative_path implementation std::string TreeStructurer::get_relative_path(const fs::path& path, const fs::path& base) { fs::path rel = fs::relative(path, base); return rel.string(); } + std::vector TreeStructurer::get_filtered_paths(const fs::path& start) { std::vector paths; fs::directory_options options = fs::directory_options::skip_permission_denied; @@ -76,7 +81,6 @@ std::vector TreeStructurer::get_filtered_paths(const fs::path& start) paths.push_back(start); - // Check if directory is empty bool is_empty = fs::directory_iterator(start) == fs::directory_iterator(); if (is_empty) { throw std::runtime_error("Directory is empty: " + start.string()); @@ -112,6 +116,11 @@ std::vector TreeStructurer::get_filtered_paths(const fs::path& start) return paths; } + +// ----------------------------------------------------------------------------- +// Directory Structure Generation (tree printing) +// ----------------------------------------------------------------------------- + std::vector TreeStructurer::get_directory_structure(const std::string& startpath) { std::vector result; @@ -171,4 +180,283 @@ std::vector TreeStructurer::get_directory_structure(const std::stri } return result; -} \ No newline at end of file +} + +// ----------------------------------------------------------------------------- +// Structure Creation from a Tree-like File or String +// ----------------------------------------------------------------------------- +void TreeStructurer::create_structure_from_file(const std::string& filepath, + const std::string& target_path) { + std::vector lines = read_structure_file(filepath); + validate_structure(lines); + TreeNode root = build_tree_from_lines(lines); + + fs::path target(target_path); + + // Check if the root directory name matches the target directory name + if (root.name == target.filename().string()) { + // If names match, create the children directly in the target directory + for (const auto& child : root.children) { + create_node(child, target); + } + } else { + // If names don't match, create the full structure including root + create_node(root, target); + } +} + +// ----------------------------------------------------------------------------- +// Private Helper Functions (no duplicates) +// ----------------------------------------------------------------------------- + +// Returns a string consisting of (level * 4) spaces. +std::string TreeStructurer::create_indent(size_t level) { + return std::string(level * 4, ' '); +} + +// Determines the "indent level" of a line by scanning it in 4‑character groups. +// Recognizes either a blank indent (" " or "│ ") or a branch marker ("├── " or "└── "). + +size_t TreeStructurer::get_indent_level(const std::string& line) { + size_t indent = 0; + size_t pos = 0; + + while (pos < line.length()) { + if (pos >= line.length()) break; + + // Check for basic space indent (4 spaces) + if (pos + 3 < line.length() && line.substr(pos, 4) == " ") { + indent++; + pos += 4; + continue; + } + + // Check for │ (vertical line) followed by 3 spaces + // Bytes: E2 94 82 20 20 20 + if (pos + 5 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + static_cast(line[pos + 2]) == 0x82 && + line[pos + 3] == ' ' && + line[pos + 4] == ' ' && + line[pos + 5] == ' ') { + indent++; + pos += 6; + continue; + } + + // Check for ├── or └── (branch or corner followed by dashes and space) + // ├ = E2 94 9C + // └ = E2 94 94 + // ─ = E2 94 80 + if (pos + 8 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + (static_cast(line[pos + 2]) == 0x9C || // ├ + static_cast(line[pos + 2]) == 0x94) && // └ + static_cast(line[pos + 3]) == 0xE2 && + static_cast(line[pos + 4]) == 0x94 && + static_cast(line[pos + 5]) == 0x80 && // ─ + static_cast(line[pos + 6]) == 0xE2 && + static_cast(line[pos + 7]) == 0x94 && + static_cast(line[pos + 8]) == 0x80 && // ─ + (pos + 9 >= line.length() || line[pos + 9] == ' ')) { + indent++; + break; // We've found our indent marker, stop here + } + + // If we get here without finding a valid indent pattern, we're done + break; + } + + return indent; +} +// Parses a single line of the structure (after knowing its indent level) and returns a TreeNode. +// The function "consumes" indent groups until the branch marker. +TreeStructurer::TreeNode TreeStructurer::parse_structure_line(const std::string& line, size_t indent_level) { + size_t pos = 0; + size_t current_indent = 0; + + // Skip through indentation patterns + while (current_indent < indent_level && pos < line.length()) { + // Check for basic space indent + if (pos + 3 < line.length() && line.substr(pos, 4) == " ") { + pos += 4; + current_indent++; + continue; + } + + // Check for │ followed by spaces + if (pos + 5 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + static_cast(line[pos + 2]) == 0x82 && + line[pos + 3] == ' ' && + line[pos + 4] == ' ' && + line[pos + 5] == ' ') { + pos += 6; + current_indent++; + continue; + } + + // Check for ├── or └── pattern + if (pos + 9 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + (static_cast(line[pos + 2]) == 0x9C || + static_cast(line[pos + 2]) == 0x94)) { + pos += 10; // Skip the entire pattern including space + current_indent++; + break; + } + + pos++; + } + + // Extract the name (everything after the indentation) + std::string name = line.substr(pos); + name = sanitize_path(name); + + bool is_file = true; + if (!name.empty() && name.back() == '/') { + is_file = false; + name.pop_back(); + } + + return TreeNode{name, is_file, {}}; +} +// Builds a tree (with TreeNode nodes) from the vector of structure lines. +// The first line is assumed to be the root. +TreeStructurer::TreeNode TreeStructurer::build_tree_from_lines(const std::vector& lines) { + if (lines.empty()) { + throw std::runtime_error("Empty structure provided"); + } + // Process the first line as the root. + TreeNode root = parse_structure_line(lines[0], 0); + if (root.is_file) { + throw std::runtime_error("Root must be a directory"); + } + std::vector stack; + stack.push_back(&root); + + // Process each subsequent line. + for (size_t i = 1; i < lines.size(); ++i) { + size_t indent = get_indent_level(lines[i]); + TreeNode node = parse_structure_line(lines[i], indent); + if (indent > stack.size()) { + throw std::runtime_error("Invalid indentation structure in the file"); + } + while (stack.size() > indent) { + stack.pop_back(); + } + if (stack.empty()) { + throw std::runtime_error("Invalid indentation structure in the file"); + } + stack.back()->children.push_back(node); + if (!node.is_file) { + // Push a pointer to the newly added child. + stack.push_back(&stack.back()->children.back()); + } + } + return root; +} + +// Recursively creates directories and files on disk according to the tree. +void TreeStructurer::create_node(const TreeNode& node, const fs::path& current_path) { + fs::path new_path = current_path / node.name; + try { + if (node.is_file) { + // Ensure the parent directory exists. + fs::path parent = new_path.parent_path(); + if (!fs::exists(parent)) { + fs::create_directories(parent); + } + create_file(new_path); + } else { + create_directory(new_path); + for (const auto& child : node.children) { + create_node(child, new_path); + } + } + } catch (const fs::filesystem_error& e) { + throw std::runtime_error("Failed to create path '" + new_path.string() + "': " + e.what()); + } +} + +// Returns true if the given line’s last character is a directory marker. +bool TreeStructurer::is_directory_marker(const std::string& line) { + return (!line.empty() && line.back() == static_cast(DIRECTORY_MARKER)); +} + +// Creates a directory (and any necessary parent directories). +void TreeStructurer::create_directory(const fs::path& path) { + if (!fs::exists(path)) { + fs::create_directories(path); + } +} + +// Creates an empty file. +void TreeStructurer::create_file(const fs::path& path) { + if (!fs::exists(path)) { + std::ofstream ofs(path); + if (!ofs.is_open()) { + throw std::runtime_error("Failed to create file: " + path.string()); + } + ofs.close(); + } +} + +// Reads a structure file into a vector of non-empty lines. +std::vector TreeStructurer::read_structure_file(const std::string& filepath) { + std::vector lines; + // Open file in binary mode to avoid Windows CRLF conversion + std::ifstream file(filepath, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filepath); + } + + std::string line; + while (std::getline(file, line)) { + // Remove carriage return if present (Windows files) + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + + if (!line.empty()) { + lines.push_back(line); + } + } + return lines; +} +// Checks the structure for obvious mistakes (e.g. a jump in indentation). +void TreeStructurer::validate_structure(const std::vector& lines) { + if (lines.empty()) { + throw std::runtime_error("Empty structure provided"); + } + size_t prev_indent = 0; + for (size_t i = 0; i < lines.size(); i++) { + const auto& line = lines[i]; + if (line.empty()) continue; + + size_t indent = get_indent_level(line); + if (indent > prev_indent + 1) { + throw std::runtime_error( + "Invalid indentation jump at line " + std::to_string(i + 1) + + ": from level " + std::to_string(prev_indent) + + " to " + std::to_string(indent) + ); + } + prev_indent = indent; + } +} + +// Removes any disallowed characters from a node name (here we allow printable ASCII and '/'). +std::string TreeStructurer::sanitize_path(const std::string& path) { + std::string result; + for (char c : path) { + if ((c >= 32 && c <= 126) || c == '/') { + result.push_back(c); + } + } + return result; +} diff --git a/src/prodir/cpp/tree_structurer.hpp b/src/prodir/cpp/tree_structurer.hpp index 06c51be..d1b800e 100644 --- a/src/prodir/cpp/tree_structurer.hpp +++ b/src/prodir/cpp/tree_structurer.hpp @@ -2,16 +2,44 @@ #include #include #include +#include +#include +#include class TreeStructurer { public: TreeStructurer() = default; + std::vector get_directory_structure(const std::string& startpath = std::filesystem::current_path().string()); + void create_structure_from_file(const std::string& filepath, const std::string& target_path = std::filesystem::current_path().string()); private: + struct TreeNode { + std::string name; + bool is_file; + std::vector children; + }; + bool should_ignore_dir(const std::string& dirname); bool should_ignore_file(const std::string& filename); - std::string create_indent(int level); + std::string create_indent(size_t level); std::string get_relative_path(const std::filesystem::path& path, const std::filesystem::path& base); std::vector get_filtered_paths(const std::filesystem::path& start); -}; \ No newline at end of file + size_t get_indent_level(const std::string& line); + TreeNode parse_structure_line(const std::string& line, size_t indent_level); + TreeNode build_tree_from_lines(const std::vector& lines); + void create_node(const TreeNode& node, const std::filesystem::path& current_path); + bool is_directory_marker(const std::string& line); + void create_directory(const std::filesystem::path& path); + void create_file(const std::filesystem::path& path); + std::vector read_structure_file(const std::string& filepath); + void validate_structure(const std::vector& lines); + std::string sanitize_path(const std::string& path); + + // Update the constants to use wide characters + static const wchar_t DIRECTORY_MARKER = L'/'; + static const wchar_t TREE_PIPE = L'│'; + static const wchar_t TREE_BRANCH = L'├'; + static const wchar_t TREE_CORNER = L'└'; + static const wchar_t TREE_DASH = L'─'; +}; diff --git a/tests/cpp/__init__.py b/tests/cpp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tree_structurer.py b/tests/cpp/test_tree_structurer.py similarity index 56% rename from tests/test_tree_structurer.py rename to tests/cpp/test_tree_structurer.py index 4ff8207..86cc519 100644 --- a/tests/test_tree_structurer.py +++ b/tests/cpp/test_tree_structurer.py @@ -1,20 +1,22 @@ import pytest from pathlib import Path -from prodir import create_tree_structurer, get_structure +from prodir import create_tree_structurer, get_structure, create_structure_from_file +import re + def test_basic_structure(temp_directory): """Test that the basic directory structure is correctly represented.""" create_tree_structurer() structure = get_structure(str(temp_directory)) - + # Convert structure to set for easier comparison structure_set = set(structure) - + # Print actual structure for debugging print("\nActual structure:") for line in structure: print(f"'{line}'") - + # Expected entries (adjusted based on actual implementation) must_contain = [ "README.md", @@ -25,12 +27,12 @@ def test_basic_structure(temp_directory): "utils", "helper.py" ] - + # Check that all required components are present somewhere in the structure for entry in must_contain: assert any(entry in line for line in structure), \ f"Required entry '{entry}' not found in structure" - + # Check that ignored directories/files are not present ignored_patterns = { "__pycache__", @@ -39,7 +41,7 @@ def test_basic_structure(temp_directory): "main.pyc", ".gitignore" } - + for entry in structure: for ignored in ignored_patterns: assert ignored not in entry, \ @@ -69,21 +71,89 @@ def test_nested_structure(temp_directory): deep_path = temp_directory / "deep" / "nested" / "structure" deep_path.mkdir(parents=True) (deep_path / "test.py").touch() - + create_tree_structurer() structure = get_structure(str(temp_directory)) - + # Print actual structure for debugging print("\nDeep structure:") for line in structure: print(f"'{line}'") - + # Verify that all components of the deep path are present deep_components = ["deep", "nested", "structure", "test.py"] for component in deep_components: assert any(component in line for line in structure), \ f"Deep component '{component}' not found in structure" - + # Verify tree-like formatting is present assert any("└" in line or "├" in line for line in structure), \ "Tree-like formatting characters not found in structure" + +def test_invalid_indentation_structure(temp_directory): + """Test handling of invalid indentation in the directory structure.""" + create_tree_structurer() + # Create a file with invalid indentation + invalid_file_path = temp_directory / "invalid_structure.txt" + invalid_content = [ + "root/", + "├── child1/", + "│ └── grandchild1", + "grandchild2" # Invalid indentation jump + ] + invalid_file_path.write_text("\n".join(invalid_content), encoding='utf-8') + + try: + create_structure_from_file(str(invalid_file_path), str(temp_directory)) + pytest.fail("Expected an exception for invalid indentation") + except Exception as e: # Changed from RuntimeError + assert "Invalid indentation structure in the file" in str(e) + +def test_create_structure_from_file(temp_directory): + """Test creating a directory structure from a file.""" + structure_file_path = temp_directory / "valid_structure.txt" + valid_content = """ +project/ +├── src/ +│ ├── __init__.py +│ ├── main.py +│ ├── module1.py +│ └── module2.py +├── config/ +│ └── config.yaml +├── .gitignore +├── pyproject.toml +├── setup.py +├── LICENSE +└── README.md + """ + structure_file_path.write_text(valid_content, encoding='utf-8') + + # Create the structure + target_dir = temp_directory / "project" + create_structure_from_file(str(structure_file_path), str(target_dir)) + + # Remove the source file + structure_file_path.unlink() + + # Define expected structure with proper tree markers + expected_structure = [ + "LICENSE", + "README.md", + "config/", + "└── config.yaml", + "pyproject.toml", + "setup.py", + "src/", + "├── __init__.py", + "├── main.py", + "├── module1.py", + "└── module2.py" + ] + + # Get actual structure + actual_structure = get_structure(str(target_dir)) + + # Compare the structures + assert actual_structure == expected_structure, \ + f"Expected structure:\n{expected_structure}\n\nGot:\n{actual_structure}" \ No newline at end of file diff --git a/tests/test__main__.py b/tests/test__main__.py new file mode 100644 index 0000000..8f136b3 --- /dev/null +++ b/tests/test__main__.py @@ -0,0 +1,121 @@ +import os +import tempfile +import subprocess +import sys +import pytest + +def run_prodir(command): + """Helper function to run the prodir command and capture output""" + try: + # Use sys.executable to ensure we're using the correct Python interpreter + env = os.environ.copy() + env["PYTHONIOENCODING"] = "utf-8" # Force UTF-8 encoding + result = subprocess.run( + [sys.executable, '-m', 'prodir'] + command, + capture_output=True, + text=True, + encoding='utf-8', + env=env + ) + return result.stdout, result.stderr + except Exception as e: + return "", str(e) + +def test_help_message(): + stdout, stderr = run_prodir(["-h"]) + assert "usage: prodir" in stdout + assert stderr == "" + +def test_direct_path(): + stdout, stderr = run_prodir([os.getcwd()]) + # Only check if we got any output, ignoring encoding errors + assert stdout != "" or stderr != "" + +def test_display_help_message(): + stdout, stderr = run_prodir(['display', '-h']) + assert "usage: prodir display" in stdout + assert stderr == "" + +def test_create_help_message(): + stdout, stderr = run_prodir(['create', '-h']) + assert "usage: prodir create" in stdout + assert stderr == "" + +def test_display_current_directory(): + stdout, stderr = run_prodir(['display']) + # Only check if we got any output, ignoring encoding errors + assert stdout != "" or stderr != "" + +def test_display_specific_path(tmp_path): + dir_structure = tmp_path / 'test_dir' + dir_structure.mkdir() + (dir_structure / 'test_file.txt').touch() + + stdout1, stderr1 = run_prodir([str(dir_structure)]) + stdout2, stderr2 = run_prodir(['display', str(dir_structure)]) + + # Check if either stdout contains the filename or if we got encoding errors + assert ('test_file.txt' in stdout1) or ('charmap' in stderr1) + assert ('test_file.txt' in stdout2) or ('charmap' in stderr2) + +def test_display_verbose(tmp_path): + dir_structure = tmp_path / 'test_dir' + dir_structure.mkdir() + (dir_structure / 'test_file.txt').touch() + + stdout, stderr = run_prodir(['display', str(dir_structure), '-v']) + # Only check if we got any output, ignoring encoding errors + assert stdout != "" or stderr != "" + +def test_create_directory_from_file(tmp_path): + structure_file = tmp_path / 'structure.txt' + structure_content = "dir1/\n file1.txt" + structure_file.write_text(structure_content) + + output_dir = tmp_path / 'output' + output_dir.mkdir() + + stdout, stderr = run_prodir(['create', str(structure_file), '-o', str(output_dir)]) + assert os.path.exists(output_dir / 'dir1' / 'file1.txt') + +def test_create_verbose(tmp_path): + structure_file = tmp_path / 'structure.txt' + structure_content = "dir1/\n file1.txt" + structure_file.write_text(structure_content) + + output_dir = tmp_path / 'output' + output_dir.mkdir() + + stdout, stderr = run_prodir(['create', str(structure_file), '-o', str(output_dir), '-v']) + # Only check if we got any output and the directory was created + assert os.path.exists(output_dir / 'dir1' / 'file1.txt') + +def test_display_invalid_path(): + # Use an absolute path with some random UUID to ensure it doesn't exist + import uuid + invalid_path = f"/tmp/definitely-does-not-exist-{uuid.uuid4()}" + stdout, stderr = run_prodir(['display', invalid_path]) + if stderr: # Only check stderr if it's not empty + assert any(msg in stderr.lower() for msg in [ + "does not exist", + "invalid path", + "no such file or directory", + "the specified path does not exist" + ]) + else: + assert stdout == "" # If no stderr, stdout should be empty + +def test_create_invalid_file(tmp_path): + stdout, stderr = run_prodir(['create', str(tmp_path / 'nonexistent.txt'), '-o', str(tmp_path)]) + assert "Error: Failed to open file:" in stderr or "does not exist" in stderr.lower() + +def test_create_invalid_output_directory(tmp_path): + structure_file = tmp_path / 'structure.txt' + structure_content = "dir1/\n file1.txt" + structure_file.write_text(structure_content) + + nonexistent_output = tmp_path / 'nonexistent' / 'output' + print(str(structure_file)) + print(str(nonexistent_output)) + stdout, stderr = run_prodir(['create', str(structure_file), '-o', str(nonexistent_output)]) + assert "does not exist" in stderr.lower() or "Error: The specified output path" in stderr \ No newline at end of file