Merge pull request 'develop' (#12) from develop into main
Reviewed-on: #12
This commit is contained in:
commit
a08519ded0
|
@ -34,4 +34,4 @@ jobs:
|
|||
VECTORDB_TOKEN: ${{ secrets.VECTORDB_TOKEN }}
|
||||
run: |
|
||||
cd VectorLoader
|
||||
python -m src.run --full
|
||||
python -m src.run
|
||||
|
|
|
@ -67,6 +67,9 @@
|
|||
"xmemory": "cpp",
|
||||
"xstring": "cpp",
|
||||
"xtr1common": "cpp",
|
||||
"xutility": "cpp"
|
||||
"xutility": "cpp",
|
||||
"fstream": "cpp",
|
||||
"iostream": "cpp",
|
||||
"codecvt": "cpp"
|
||||
}
|
||||
}
|
83
README.md
83
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`
|
|
@ -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 = []
|
||||
|
|
2
setup.py
2
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"),
|
||||
|
|
|
@ -1 +1,11 @@
|
|||
from prodir._tree_structurer import create_tree_structurer, get_structure
|
||||
from prodir._tree_structurer import (
|
||||
create_tree_structurer,
|
||||
get_structure,
|
||||
create_structure_from_file
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'create_tree_structurer',
|
||||
'get_structure',
|
||||
'create_structure_from_file'
|
||||
]
|
|
@ -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()
|
||||
main()
|
||||
|
|
|
@ -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<std::string> 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;
|
||||
}
|
|
@ -1,6 +1,12 @@
|
|||
#include "tree_structurer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
|
||||
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<fs::path> TreeStructurer::get_filtered_paths(const fs::path& start) {
|
||||
std::vector<fs::path> paths;
|
||||
fs::directory_options options = fs::directory_options::skip_permission_denied;
|
||||
|
@ -76,7 +81,6 @@ std::vector<fs::path> 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<fs::path> TreeStructurer::get_filtered_paths(const fs::path& start)
|
|||
return paths;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Directory Structure Generation (tree printing)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> TreeStructurer::get_directory_structure(const std::string& startpath) {
|
||||
std::vector<std::string> result;
|
||||
|
||||
|
@ -171,4 +180,283 @@ std::vector<std::string> TreeStructurer::get_directory_structure(const std::stri
|
|||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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<std::string> 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<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
static_cast<unsigned char>(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<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
(static_cast<unsigned char>(line[pos + 2]) == 0x9C || // ├
|
||||
static_cast<unsigned char>(line[pos + 2]) == 0x94) && // └
|
||||
static_cast<unsigned char>(line[pos + 3]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 4]) == 0x94 &&
|
||||
static_cast<unsigned char>(line[pos + 5]) == 0x80 && // ─
|
||||
static_cast<unsigned char>(line[pos + 6]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 7]) == 0x94 &&
|
||||
static_cast<unsigned char>(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<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
static_cast<unsigned char>(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<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
(static_cast<unsigned char>(line[pos + 2]) == 0x9C ||
|
||||
static_cast<unsigned char>(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<std::string>& 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<TreeNode*> 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<char>(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<std::string> TreeStructurer::read_structure_file(const std::string& filepath) {
|
||||
std::vector<std::string> 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<std::string>& 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;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,44 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
|
||||
class TreeStructurer {
|
||||
public:
|
||||
TreeStructurer() = default;
|
||||
|
||||
std::vector<std::string> 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<TreeNode> 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<std::filesystem::path> get_filtered_paths(const std::filesystem::path& start);
|
||||
};
|
||||
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<std::string>& 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<std::string> read_structure_file(const std::string& filepath);
|
||||
void validate_structure(const std::vector<std::string>& 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'─';
|
||||
};
|
||||
|
|
|
@ -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}"
|
|
@ -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
|
Loading…
Reference in New Issue