diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..015774b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "editor.formatOnPaste": true,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": true,
+ "source.fixAll": true
+ },
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.black-formatter"
+ },
+ "python.defaultInterpreterPath": "${workspaceRoot}/.venv/bin/python",
+ "python.terminal.focusAfterLaunch": true,
+ "python.testing.unittestEnabled": true,
+ "python.analysis.autoFormatStrings": true,
+ "python.analysis.autoImportCompletions": true,
+ "autoDocstring.startOnNewLine": true
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..71eb99a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.1.0] - 2023-09-05
+
+### Added
+
+- Helpers around file paths.
diff --git a/README.md b/README.md
index 59b92e1..7f0f75b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,204 @@
# py-toolbox
-A set of utilities for Python projects
+
+`py-toolbox` is a set of utilities for Python projects
+
+
+
+- [Requirements](#Requirements)
+- [Installation](#Installation)
+- [Development](#Development)
+ - [Code style](#Codestyle)
+ - [Testing](#Testing)
+- [License](#License)
+
+
+
+
+## Requirements
+
+The toolbox has been written in **`Python 3`**, and it needs version `3.7`.
+
+The dependencies are managed by `pip` using the file `requirements.txt`.
+
+## Installation
+
+To add `py-toolbox` to your project, run the following command:
+
+```sh
+pip install git+https://github.com/jsconan/py-toolbox.git
+```
+
+## Usage
+
+`py-toolbox` offers several utilities per domain.
+
+### Files
+
+The package `toolbox.files` offers file related utilities.
+
+- `create_file_path(path: str) -> bool`:
+
+ Creates the parent path for a file.
+
+ **Note:** exceptions are caught internally, the function will always
+ return either with `True` in case of success, or `False` otherwise.
+
+ Args:
+
+ - path (`str`): The path to the file.
+
+ Returns:
+
+ - `bool`: `True` if the path has been created, `False` otherwise.
+
+ ***
+
+- `delete_path(path: str) -> bool`:
+
+ Deletes the file or the folder at the given path.
+
+ If this is a folder, it must be empty.
+
+ **Note:** exceptions are caught internally, the function will always
+ return either with `True` in case of success, or `False` otherwise.
+
+ Args:
+
+ - path (`str`): The path to the file or folder to delete.
+
+ Returns:
+
+ - `bool`: `True` if the path has been deleted, `False` otherwise.
+
+ ***
+
+- `get_application_name() -> str`:
+
+ Gets the name of the application, based on the root folder.
+
+ Returns:
+
+ - `str`: The name of the application.
+
+ ***
+
+- `get_application_path() -> PurePath`:
+
+ Gets the path to the application's root.
+
+ Returns:
+
+ - `PurePath`: The path to the application's root.
+
+ ***
+
+- `get_file_path(relative) -> PurePath`:
+
+ Gets a full path for a file inside the application.
+
+ Args:
+
+ - relative (`str`): The internal path the file from the application's root.
+
+ Returns:
+
+ - `PurePath`: The full path.
+
+ ***
+
+- `get_module_folder_path(name: str) -> PurePath()`:
+
+ Gets the path to the folder containing the given module.
+
+ Args:
+
+ - name (`str`): The module for which get the path.
+
+ Returns:
+
+ - `PurePath`|`None`: The path to the folder containing the given module.
+
+ ***
+
+- `get_module_path(name: str) -> PurePath()`:
+
+ Gets the path to the given module.
+
+ Args:
+
+ - name (`str`): The module for which get the path.
+
+ Returns:
+
+ - `PurePath`: The path to the module.
+
+ ***
+
+## Development
+
+Check out the repository:
+
+```sh
+git clone git@github.com:jsconan/py-toolbox.git
+```
+
+Then, create the virtual env and install the dependencies:
+
+```sh
+cd py-toolbox
+python3 -m venv ".venv"
+source "./venv/bin/activate"
+pip install -r requirements.txt
+```
+
+**Note:** For deactivating the virtual env, call the command `deactivate`.
+
+**Automating the environment activation/deactivation**
+
+For activating the virtual env automatically when entering the project folder, and deactivating it when leaving the folder, you can add this snippet to you shell profile:
+
+```sh
+cd() {
+ builtin cd "$@"
+
+ local venv=".venv"
+
+ # If a Python virtualenv is active, deactivate it if the new folder is outside
+ if [[ -v VIRTUAL_ENV ]] ; then
+ local parent=$(dirname "${VIRTUAL_ENV}")
+ if [[ "${PWD}"/ != "${parent}"/* ]] ; then
+ deactivate
+ fi
+ fi
+
+ # If a Python env folder is found then activate the virtualenv
+ if [[ -d "./${venv}" ]] ; then
+ # Is it a Python venv?
+ if [[ -f "./${venv}/bin/activate" ]] ; then
+ source "./${venv}/bin/activate"
+ fi
+ fi
+}
+```
+
+### Code style
+
+Code is linted using PyLint and formatted using Black.
+
+### Testing
+
+Each module comes with unit tests, by convention, a `test` folder must be added to each package
+
+Unit tests are made using `unittest`. To run them:
+
+```sh
+python3 -m unittest
+```
+
+## License
+
+Copyright (c) 2023 Jean-Sébastien CONAN
+Distributed under the MIT License (See LICENSE file or copy at [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)).
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ae320a8
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,20 @@
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "py-toolbox"
+version = "0.1.0"
+authors = [{ name = "Jean-Sébastien CONAN", email = "jsconan@gmail.com" }]
+description = "A set of utilities for Python projects"
+readme = "README.md"
+requires-python = ">=3.7"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+]
+
+[project.urls]
+"Homepage" = "https://github.com/jsconan/py-toolbox"
+"Bug Tracker" = "https://github.com/jsconan/py-toolbox/issues"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4943664
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,9 @@
+astroid==2.15.6
+dill==0.3.7
+isort==5.12.0
+lazy-object-proxy==1.9.0
+mccabe==0.7.0
+platformdirs==3.10.0
+pylint==2.17.5
+tomlkit==0.12.1
+wrapt==1.15.0
diff --git a/toolbox/__init__.py b/toolbox/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/toolbox/files/__init__.py b/toolbox/files/__init__.py
new file mode 100644
index 0000000..440cd81
--- /dev/null
+++ b/toolbox/files/__init__.py
@@ -0,0 +1,12 @@
+"""
+Entry point for the `files` package.
+"""
+from toolbox.files.path import (
+ create_file_path,
+ delete_path,
+ get_application_name,
+ get_application_path,
+ get_file_path,
+ get_module_folder_path,
+ get_module_path,
+)
diff --git a/toolbox/files/path.py b/toolbox/files/path.py
new file mode 100644
index 0000000..4346655
--- /dev/null
+++ b/toolbox/files/path.py
@@ -0,0 +1,124 @@
+"""
+A collection of utilities around file paths.
+"""
+import os
+import sys
+from pathlib import PurePath
+
+
+def get_module_path(name: str) -> PurePath():
+ """
+ Gets the path to the given module.
+
+ Args:
+ - name (str): The module for which get the path.
+
+ Returns:
+ PurePath: The path to the module.
+ """
+ if name in sys.modules:
+ return PurePath(sys.modules[name].__file__)
+
+ return PurePath()
+
+
+def get_module_folder_path(name: str) -> PurePath():
+ """
+ Gets the path to the folder containing the given module.
+
+ Args:
+ - name (str): The module for which get the path.
+
+ Returns:
+ PurePath|None: The path to the folder containing the given module.
+ """
+ return get_module_path(name).parent
+
+
+def get_application_path() -> PurePath:
+ """
+ Gets the path to the application's root.
+
+ Returns:
+ PurePath: The path to the application's root.
+ """
+ return get_module_folder_path("__main__")
+
+
+def get_application_name() -> str:
+ """
+ Gets the name of the application, based on the root folder.
+
+ Returns:
+ - str: The name of the application.
+ """
+ return get_application_path().name
+
+
+def get_file_path(relative) -> PurePath:
+ """
+ Gets a full path for a file inside the application.
+
+ Args:
+ - relative (str): The internal path the file from the application's root.
+
+ Returns:
+ PurePath: The full path.
+ """
+ return get_application_path().joinpath(relative)
+
+
+def create_file_path(path: str) -> bool:
+ """
+ Creates the parent path for a file.
+
+ Note: exceptions are caught internally, the function will always
+ return either with `True` in case of success, or `False` otherwise.
+
+ Args:
+ - path (str): The path to the file.
+
+ Returns:
+ bool: `True` if the path has been created, `False` otherwise.
+ """
+ folder = str(PurePath(path).parent)
+
+ try:
+ if not os.path.isdir(folder):
+ os.makedirs(folder)
+ return True
+
+ except OSError:
+ return False
+
+ return False
+
+
+def delete_path(path: str) -> bool:
+ """
+ Deletes the file or the folder at the given path.
+
+ If this is a folder, it must be empty.
+
+ Note: exceptions are caught internally, the function will always
+ return either with `True` in case of success, or `False` otherwise.
+
+ Args:
+ - path (str): The path to the file or folder to delete.
+
+ Returns:
+ bool: `True` if the path has been deleted, `False` otherwise.
+ """
+ try:
+ if os.path.isdir(path):
+ os.rmdir(path)
+ else:
+ os.remove(path)
+
+ except FileNotFoundError:
+ return False
+
+ except OSError:
+ return False
+
+ return True
diff --git a/toolbox/files/test/__init__.py b/toolbox/files/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/toolbox/files/test/test_path.py b/toolbox/files/test/test_path.py
new file mode 100644
index 0000000..dc82ad8
--- /dev/null
+++ b/toolbox/files/test/test_path.py
@@ -0,0 +1,168 @@
+"""
+Test the collection of utilities around file paths.
+"""
+import sys
+import unittest
+from pathlib import PurePath
+from unittest.mock import Mock, patch
+
+from toolbox.files import path
+
+
+class TestFilePaths(unittest.TestCase):
+ """
+ Test suite for the file path helpers.
+ """
+
+ def test_get_module_path(self):
+ """
+ Tests the helper get_module_path().
+ """
+ mock_namespace = "app.package.module"
+ mock_path = "/root/app/package/module.py"
+
+ module_mock = Mock()
+ module_mock.__file__ = mock_path
+
+ with patch.dict(sys.modules, {mock_namespace: module_mock}):
+ result = path.get_module_path(mock_namespace)
+ self.assertIsInstance(result, PurePath)
+ self.assertEqual(str(result), mock_path)
+
+ def test_get_module_folder(self):
+ """
+ Tests the helper get_module_folder().
+ """
+ mock_namespace = "app.package.module"
+ mock_path = "/root/app/package/module.py"
+ mock_folder = "/root/app/package"
+
+ module_mock = Mock()
+ module_mock.__file__ = mock_path
+
+ with patch.dict(sys.modules, {mock_namespace: module_mock}):
+ result = path.get_module_folder_path(mock_namespace)
+ self.assertIsInstance(result, PurePath)
+ self.assertEqual(str(result), mock_folder)
+
+ def test_get_application_path(self):
+ """
+ Tests the helper get_application_path().
+ """
+ mock_namespace = "__main__"
+ mock_path = "/root/app/main.py"
+ mock_root = "/root/app"
+
+ module_mock = Mock()
+ module_mock.__file__ = mock_path
+
+ with patch.dict(sys.modules, {mock_namespace: module_mock}):
+ result = path.get_application_path()
+ self.assertIsInstance(result, PurePath)
+ self.assertEqual(str(result), mock_root)
+
+ def test_get_application_name(self):
+ """
+ Tests the helper get_application_name().
+ """
+ mock_namespace = "__main__"
+ mock_path = "/root/app/main.py"
+ mock_name = "app"
+
+ module_mock = Mock()
+ module_mock.__file__ = mock_path
+
+ with patch.dict(sys.modules, {mock_namespace: module_mock}):
+ result = path.get_application_name()
+ self.assertIsInstance(result, str)
+ self.assertEqual(result, mock_name)
+
+ def test_get_file_path(self):
+ """
+ Tests the helper get_file_path().
+ """
+ mock_namespace = "__main__"
+ mock_path = "/root/app/main.py"
+ mock_root = "/root/app"
+ test_param = "subfolder/file"
+
+ module_mock = Mock()
+ module_mock.__file__ = mock_path
+
+ with patch.dict(sys.modules, {mock_namespace: module_mock}):
+ result = path.get_file_path(test_param)
+ self.assertIsInstance(result, PurePath)
+ self.assertEqual(str(result), f"{mock_root}/{test_param}")
+
+ def test_create_file_path(self):
+ """
+ Test the helper create_file_path().
+ """
+ folder_path = "/root/folder"
+ file_path = "/root/folder/file"
+
+ with patch("os.path.isdir", return_value=True) as mock:
+ result = path.create_file_path(file_path)
+ self.assertFalse(result)
+ mock.assert_called_once_with(folder_path)
+
+ with patch("os.path.isdir", return_value=False) as mock_isdir:
+ with patch("os.makedirs") as mock_makedirs:
+ result = path.create_file_path(file_path)
+ self.assertTrue(result)
+ mock_isdir.assert_called_once_with(folder_path)
+ mock_makedirs.assert_called_once_with(folder_path)
+
+ with patch("os.path.isdir", return_value=False) as mock_isdir:
+ with patch("os.makedirs", side_effect=OSError("error")) as mock_makedirs:
+ result = path.create_file_path(file_path)
+ self.assertFalse(result)
+ mock_isdir.assert_called_once_with(folder_path)
+ mock_makedirs.assert_called_once_with(folder_path)
+ mock_makedirs.assert_called_once_with(folder_path)
+ mock_makedirs.assert_called_once_with(folder_path)
+ mock_makedirs.assert_called_once_with(folder_path)
+
+ def test_delete_path_file(self):
+ """
+ Tests the helper delete_path() for deleting a file.
+ """
+ file_path = "/root/folder/file"
+
+ with patch("os.path.isdir", return_value=False):
+ with patch("os.remove") as mock:
+ result = path.delete_path(file_path)
+ self.assertTrue(result)
+ mock.assert_called_once_with(file_path)
+
+ with patch("os.remove", side_effect=OSError("error")) as mock:
+ result = path.delete_path(file_path)
+ self.assertFalse(result)
+ mock.assert_called_once_with(file_path)
+
+ with patch("os.remove", side_effect=FileNotFoundError("error")) as mock:
+ result = path.delete_path(file_path)
+ self.assertFalse(result)
+ mock.assert_called_once_with(file_path)
+
+ def test_delete_path_folder(self):
+ """
+ Tests the helper delete_path() for deleting a folder.
+ """
+ file_path = "/root/folder/subfolder"
+
+ with patch("os.path.isdir", return_value=True):
+ with patch("os.rmdir") as mock:
+ result = path.delete_path(file_path)
+ self.assertTrue(result)
+ mock.assert_called_once_with(file_path)
+
+ with patch("os.rmdir", side_effect=OSError("error")) as mock:
+ result = path.delete_path(file_path)
+ self.assertFalse(result)
+ mock.assert_called_once_with(file_path)
+
+ with patch("os.rmdir", side_effect=FileNotFoundError("error")) as mock:
+ result = path.delete_path(file_path)
+ self.assertFalse(result)
+ mock.assert_called_once_with(file_path)