diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 79a76fd..8a5afb3 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -24,7 +24,7 @@ How would users interact with this feature? If applicable, provide example code
```python
# Example code demonstrating how the feature would be used
-from my_python_package import new_feature
+from greeting_toolkit import new_feature
new_feature.do_something()
```
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 3bc36ab..91a0ba5 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -34,7 +34,7 @@ jobs:
- name: Run tests with coverage
run: |
- poetry run pytest --cov=my_python_package --cov-report=xml --cov-report=term
+ poetry run pytest --cov=greeting_toolkit --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..f1ad68e
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,33 @@
+name: Deploy Documentation
+
+on:
+ push:
+ branches: ["main"]
+ workflow_dispatch:
+
+jobs:
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - name: Install Poetry
+ run: |
+ curl -sSL https://install.python-poetry.org | python3 -
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
+ - name: Install dependencies
+ run: |
+ poetry install --with docs
+ - name: Build docs
+ run: |
+ poetry run make -C docs html
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: docs/_build/html
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8c29d56..fcb3929 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -205,8 +205,8 @@ jobs:
# Update the links at the bottom
if grep -q "\[unreleased\]:" CHANGELOG.md; then
- sed -i "s|\[unreleased\]:.*|[unreleased]: https://github.com/DiogoRibeiro7/my_python_package/compare/v${{ steps.extract_version.outputs.version }}...HEAD|" CHANGELOG.md
- sed -i "/\[unreleased\]:/a [{{ steps.extract_version.outputs.version }}]: https://github.com/DiogoRibeiro7/my_python_package/compare/v$(git describe --tags --abbrev=0 ${{ github.ref_name }}^)...v${{ steps.extract_version.outputs.version }}" CHANGELOG.md
+ sed -i "s|\[unreleased\]:.*|[unreleased]: https://github.com/DiogoRibeiro7/greeting-toolkit/compare/v${{ steps.extract_version.outputs.version }}...HEAD|" CHANGELOG.md
+ sed -i "/\[unreleased\]:/a [{{ steps.extract_version.outputs.version }}]: https://github.com/DiogoRibeiro7/greeting-toolkit/compare/v$(git describe --tags --abbrev=0 ${{ github.ref_name }}^)...v${{ steps.extract_version.outputs.version }}" CHANGELOG.md
fi
fi
fi
@@ -215,4 +215,4 @@ jobs:
if: success()
run: |
echo "โ
Package v${{ steps.extract_version.outputs.version }} has been released to PyPI!"
- echo "๐ฆ https://pypi.org/project/my-python-package/${{ steps.extract_version.outputs.version }}/"
+ echo "๐ฆ https://pypi.org/project/greeting-toolkit/${{ steps.extract_version.outputs.version }}/"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e2fca88..159b5b8 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -34,7 +34,7 @@ jobs:
- name: Run tests with coverage
run: |
- poetry run pytest --cov=my_python_package --cov-report=xml
+ poetry run pytest --cov=greeting_toolkit --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4bd8932..dedf755 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
+ exclude: ^mkdocs\.yaml$
- id: check-toml
- id: check-added-large-files
- id: check-ast
@@ -15,38 +16,42 @@ repos:
args: [--fix=lf]
- repo: https://github.com/pycqa/isort
- rev: 5.13.2
+ rev: 6.0.1
hooks:
- id: isort
args: [--profile, black, --filter-files]
- repo: https://github.com/psf/black
- rev: 24.2.0
+ rev: 25.1.0
hooks:
- id: black
args: [--line-length=100]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.3.0
+ rev: v0.12.9
hooks:
- id: ruff
args: [--fix]
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.9.0
+ rev: v1.17.1
hooks:
- id: mypy
- additional_dependencies: [types-all]
+ files: ^src/
+ # Use only the required typing stubs instead of the heavy types-all package.
+ additional_dependencies:
+ # Stub package for setuptools (includes pkg_resources types)
+ - types-setuptools
- repo: https://github.com/PyCQA/bandit
- rev: 1.7.7
+ rev: 1.8.6
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
exclude: "tests/"
- repo: https://github.com/asottile/pyupgrade
- rev: v3.15.1
+ rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py310-plus]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e23127..23f043e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Fixed GitHub Actions permissions for the code coverage workflow
+### Migration
+
+- Renamed package from `my_python_package` to `greeting-toolkit`. Update imports to `greeting_toolkit` and CLI usage to `greeting-toolkit`.
+
## [0.1.1] - 2025-08-14
### Added
@@ -48,6 +52,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Basic project structure
- MIT License
-[0.1.0]: https://github.com/DiogoRibeiro7/my_python_package/releases/tag/v0.1.0
-[0.1.1]: https://github.com/DiogoRibeiro7/my_python_package/compare/v0.1.0...v0.1.1
-[unreleased]: https://github.com/DiogoRibeiro7/my_python_package/compare/v0.1.1...HEAD
+[0.1.0]: https://github.com/DiogoRibeiro7/greeting-toolkit/releases/tag/v0.1.0
+[0.1.1]: https://github.com/DiogoRibeiro7/greeting-toolkit/compare/v0.1.0...v0.1.1
+[unreleased]: https://github.com/DiogoRibeiro7/greeting-toolkit/compare/v0.1.1...HEAD
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f51de78..7bc10e1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,8 @@
-# Contributing to my_python_package
+# Contributing to greeting-toolkit
-Thank you for considering contributing to my_python_package! This document provides guidelines and instructions for contributing.
+Thank you for considering contributing to greeting-toolkit! This document provides guidelines and instructions for contributing.
+
+> **Migration note:** The project was renamed from `my_python_package` to `greeting-toolkit`. Update imports to `greeting_toolkit` and CLI usage to `greeting-toolkit`.
## Code of Conduct
@@ -43,8 +45,8 @@ For feature requests, please use the feature request template. Include:
1. Clone the repository:
```bash
- git clone https://github.com/DiogoRibeiro7/my_python_package.git
- cd my_python_package
+ git clone https://github.com/DiogoRibeiro7/greeting-toolkit.git
+ cd greeting-toolkit
```
2. Set up the development environment:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f388a5b..8208265 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -1,6 +1,6 @@
# Contributors
-This file lists the contributors to the `my_python_package` project.
+This file lists the contributors to the `greeting-toolkit` project.
## Core Contributors
@@ -17,7 +17,7 @@ Template for new contributors:
## How to Contribute
-Thank you for considering contributing to `my_python_package`! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project.
+Thank you for considering contributing to `greeting-toolkit`! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for details on how to contribute to this project.
## Contributor License Agreement
diff --git a/Dockerfile b/Dockerfile
index a18fa7b..252ce3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -40,4 +40,4 @@ RUN poetry install
ENTRYPOINT ["poetry", "run"]
# Default command
-CMD ["python", "-m", "my_python_package"]
+CMD ["python", "-m", "greeting_toolkit"]
diff --git a/Makefile b/Makefile
index 662f7cb..69a3f4c 100644
--- a/Makefile
+++ b/Makefile
@@ -46,7 +46,7 @@ test:
poetry run pytest
test-cov:
- poetry run pytest --cov=my_python_package --cov-report=term-missing
+ poetry run pytest --cov=greeting_toolkit --cov-report=term-missing
tox:
poetry run tox
diff --git a/README.md b/README.md
index b491091..1331c60 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,10 @@
-# my_python_package
+# greeting-toolkit
-[ ](https://pypi.org/project/my_python_package/)
-[](https://pypi.org/project/my_python_package/)
-[](https://github.com/DiogoRibeiro7/my_python_package/actions/workflows/test.yml)
-[](https://codecov.io/gh/DiogoRibeiro7/my_python_package)
+[](https://pypi.org/project/greeting-toolkit/)
+
+[](https://pypi.org/project/greeting-toolkit/)
+[](https://github.com/DiogoRibeiro7/greeting-toolkit/actions/workflows/test.yml)
+[](https://codecov.io/gh/DiogoRibeiro7/greeting-toolkit)
[](https://opensource.org/licenses/MIT)
[](https://github.com/psf/black)
[](https://github.com/astral-sh/ruff)
@@ -11,13 +12,13 @@
[](https://github.com/PyCQA/bandit)
[](https://github.com/pre-commit/pre-commit)
-A minimal but production-ready Python package scaffold configured for publishing to [PyPI](https://pypi.org).
+**greeting-toolkit** is a minimal but production-ready Python package scaffold configured for publishing to [PyPI](https://pypi.org).
## Features
- ๐ Modern Python packaging with Poetry
- ๐ง Configurable greeting functions with multiple formatting options
-- ๐งช Comprehensive testing suite with 100% coverage
+- ๐งช Comprehensive testing suite with over 95% coverage
- ๐ Continuous Integration workflows for testing, coverage, and releases
- ๐ ๏ธ Code quality tools preconfigured (black, ruff, mypy, isort, pre-commit)
- ๐ Complete documentation with doctests
@@ -31,18 +32,18 @@ A minimal but production-ready Python package scaffold configured for publishing
```bash
# Using pip
-pip install my_python_package
+pip install greeting-toolkit
# Using Poetry
-poetry add my_python_package
+poetry add greeting-toolkit
```
### For Development
```bash
# Clone the repository
-git clone https://github.com/DiogoRibeiro7/my_python_package.git
-cd my_python_package
+git clone https://github.com/DiogoRibeiro7/greeting-toolkit.git
+cd greeting-toolkit
# Using Poetry (recommended)
poetry install
@@ -59,7 +60,7 @@ make setup
### Basic Greeting
```python
-from my_python_package import hello
+from greeting_toolkit import hello
# Basic usage
greeting = hello("World")
@@ -73,7 +74,7 @@ print(custom) # Output: Hi, Python!
### Formatted Greetings
```python
-from my_python_package import format_greeting
+from greeting_toolkit import format_greeting
# Default formatting
print(format_greeting("World")) # Output: Hello, World!
@@ -93,7 +94,7 @@ print(format_greeting("Very Long Name", max_length=15)) # Output: Hello, Very..
### Multiple Greetings
```python
-from my_python_package import create_greeting_list
+from greeting_toolkit import create_greeting_list
# Greet multiple people
greetings = create_greeting_list(["Alice", "Bob", "Charlie"])
@@ -104,7 +105,7 @@ for greeting in greetings:
### Context-Aware Greetings
```python
-from my_python_package import generate_greeting
+from greeting_toolkit import generate_greeting
# Time-based greeting (morning/afternoon/evening)
print(generate_greeting("World", time_based=True))
@@ -116,7 +117,7 @@ print(generate_greeting("Mrs. Smith", formal=True))
### Random Greetings
```python
-from my_python_package import random_greeting
+from greeting_toolkit import random_greeting
# Get a random greeting
print(random_greeting("World")) # Different greeting each time
@@ -128,19 +129,19 @@ The package also provides a command-line interface:
```bash
# Basic greeting
-my-python-package hello World
+greeting-toolkit hello World
# Random greeting
-my-python-package random World
+greeting-toolkit random World
# Time-based greeting
-my-python-package time World --formal
+greeting-toolkit time World --formal
# Multiple names
-my-python-package multi Alice Bob Charlie --greeting "Greetings"
+greeting-toolkit multi Alice Bob Charlie --greeting "Greetings"
# Formatted greeting
-my-python-package format World --greeting "Welcome" --uppercase --max-length 15
+greeting-toolkit format World --greeting "Welcome" --uppercase --max-length 15
```
## Development
@@ -276,7 +277,7 @@ The repository includes GitHub Actions for:
## Project Structure
```text
-my_python_package/
+greeting_toolkit/
โโโ pyproject.toml # Project metadata, dependencies
โโโ README.md # Project overview
โโโ LICENSE # MIT license
@@ -294,7 +295,7 @@ my_python_package/
โโโ requirements.txt # Dependencies for simple installation
โโโ dev-requirements.txt # Development dependencies
โโโ src/
-โ โโโ my_python_package/ # Package source code
+โ โโโ greeting_toolkit/ # Package source code
โ โโโ __init__.py # Package exports
โ โโโ __main__.py # Module execution entry point
โ โโโ core.py # Core greeting functionality
diff --git a/bandit-results.json b/bandit-results.json
new file mode 100644
index 0000000..7a67223
--- /dev/null
+++ b/bandit-results.json
@@ -0,0 +1,98 @@
+{
+ "errors": [],
+ "generated_at": "2025-08-16T07:37:31Z",
+ "metrics": {
+ "_totals": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 1016,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\__init__.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 48,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\__main__.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 5,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\cli.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 233,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\config.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 296,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\core.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 322,
+ "nosec": 0,
+ "skipped_tests": 0
+ },
+ "src/greeting_toolkit\\logging.py": {
+ "CONFIDENCE.HIGH": 0,
+ "CONFIDENCE.LOW": 0,
+ "CONFIDENCE.MEDIUM": 0,
+ "CONFIDENCE.UNDEFINED": 0,
+ "SEVERITY.HIGH": 0,
+ "SEVERITY.LOW": 0,
+ "SEVERITY.MEDIUM": 0,
+ "SEVERITY.UNDEFINED": 0,
+ "loc": 112,
+ "nosec": 0,
+ "skipped_tests": 0
+ }
+ },
+ "results": []
+}
\ No newline at end of file
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 5289fe8..94ec533 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -13,4 +13,4 @@ pytest-sugar>=1.0.0
pre-commit>=3.6.2
tox>=4.13.0
pdoc>=14.3.0
-types-all>=1.0.0
+types-setuptools>=0.1
diff --git a/docker-compose.yml b/docker-compose.yml
index 2d30ddd..a1787f9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,7 +7,7 @@ services:
dockerfile: Dockerfile
volumes:
- .:/opt/pysetup
- command: python -m my_python_package
+ command: python -m greeting_toolkit
test:
build:
diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css
index 5dc2e7d..a97c897 100644
--- a/docs/_static/css/custom.css
+++ b/docs/_static/css/custom.css
@@ -1,4 +1,4 @@
-/* Custom styles for my_python_package documentation */
+/* Custom styles for greeting_toolkit documentation */
/* Improve code block appearance */
div[class^="highlight"] {
diff --git a/docs/_static/js/custom.js b/docs/_static/js/custom.js
index 9381881..b4fd83f 100644
--- a/docs/_static/js/custom.js
+++ b/docs/_static/js/custom.js
@@ -1,4 +1,4 @@
-// Custom JavaScript for my_python_package documentation
+// Custom JavaScript for greeting_toolkit documentation
document.addEventListener('DOMContentLoaded', function() {
// Add copy buttons to code blocks
diff --git a/docs/api.md b/docs/api.md
index 590f283..280d98a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,31 +1,31 @@
# API Reference
-This page documents the public API of `my_python_package`.
+This page documents the public API of `greeting_toolkit`.
## Core Functions
-::: my_python_package.core
+::: greeting_toolkit.core
options:
show_root_heading: true
show_source: true
## Configuration
-::: my_python_package.config
+::: greeting_toolkit.config
options:
show_root_heading: true
show_source: true
## Logging
-::: my_python_package.logging
+::: greeting_toolkit.logging
options:
show_root_heading: true
show_source: true
## Command Line Interface
-::: my_python_package.cli
+::: greeting_toolkit.cli
options:
show_root_heading: true
show_source: true
diff --git a/docs/cli.rst b/docs/cli.rst
index 2163305..ec5271b 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -2,7 +2,7 @@
Command Line Interface
=======================
-``my_python_package`` provides a command-line interface (CLI) for easy access to its functionality.
+``greeting_toolkit`` provides a command-line interface (CLI) for easy access to its functionality.
Basic Usage
----------
@@ -11,7 +11,7 @@ The basic syntax for the CLI is:
.. code-block:: bash
- my-python-package COMMAND [ARGS] [OPTIONS]
+ greeting-toolkit COMMAND [ARGS] [OPTIONS]
Available Commands
-----------------
@@ -23,16 +23,16 @@ Generate a simple greeting:
.. code-block:: bash
- my-python-package hello NAME [--greeting GREETING]
+ greeting-toolkit hello NAME [--greeting GREETING]
Examples:
.. code-block:: bash
- my-python-package hello World
+ greeting-toolkit hello World
# Output: Hello, World!
- my-python-package hello Python --greeting Hi
+ greeting-toolkit hello Python --greeting Hi
# Output: Hi, Python!
random
@@ -42,13 +42,13 @@ Generate a random greeting:
.. code-block:: bash
- my-python-package random NAME
+ greeting-toolkit random NAME
Example:
.. code-block:: bash
- my-python-package random World
+ greeting-toolkit random World
# Output varies with each run
time
@@ -58,19 +58,19 @@ Generate a time-based greeting:
.. code-block:: bash
- my-python-package time NAME [--formal]
+ greeting-toolkit time NAME [--formal]
Examples:
.. code-block:: bash
- my-python-package time World
+ greeting-toolkit time World
# Output depends on time of day:
# Morning: "Good morning, World!"
# Afternoon: "Good afternoon, World!"
# Evening: "Good evening, World!"
- my-python-package time Mrs.Smith --formal
+ greeting-toolkit time Mrs.Smith --formal
# Output: "Good day, Mr./Ms. Mrs.Smith!"
format
@@ -80,7 +80,7 @@ Format a greeting with various options:
.. code-block:: bash
- my-python-package format NAME
+ greeting-toolkit format NAME
[--greeting GREETING]
[--punctuation PUNCTUATION]
[--uppercase]
@@ -90,13 +90,13 @@ Examples:
.. code-block:: bash
- my-python-package format World
+ greeting-toolkit format World
# Output: Hello, World!
- my-python-package format World --greeting Welcome --punctuation "!!!" --uppercase
+ greeting-toolkit format World --greeting Welcome --punctuation "!!!" --uppercase
# Output: WELCOME, WORLD!!!
- my-python-package format "Very Long Name" --max-length 15
+ greeting-toolkit format "Very Long Name" --max-length 15
# Output: Hello, Very...
multi
@@ -106,19 +106,19 @@ Greet multiple names:
.. code-block:: bash
- my-python-package multi NAME1 NAME2 ... [--greeting GREETING]
+ greeting-toolkit multi NAME1 NAME2 ... [--greeting GREETING]
Example:
.. code-block:: bash
- my-python-package multi Alice Bob Charlie
+ greeting-toolkit multi Alice Bob Charlie
# Output:
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!
- my-python-package multi Alice Bob --greeting "Greetings"
+ greeting-toolkit multi Alice Bob --greeting "Greetings"
# Output:
# Greetings, Alice!
# Greetings, Bob!
@@ -130,7 +130,7 @@ Manage configuration settings:
.. code-block:: bash
- my-python-package config SUBCOMMAND [OPTIONS]
+ greeting-toolkit config SUBCOMMAND [OPTIONS]
Subcommands:
@@ -145,28 +145,28 @@ Examples:
.. code-block:: bash
# Show current configuration
- my-python-package config show
+ greeting-toolkit config show
# Set default greeting
- my-python-package config set --greeting "Howdy"
+ greeting-toolkit config set --greeting "Howdy"
# Set default punctuation
- my-python-package config set --punctuation "?"
+ greeting-toolkit config set --punctuation "?"
# Set formal title
- my-python-package config set --title "Dr. "
+ greeting-toolkit config set --title "Dr. "
# Set maximum name length
- my-python-package config set --max-name-length 30
+ greeting-toolkit config set --max-name-length 30
# Add a greeting
- my-python-package config add-greeting "Salutations"
+ greeting-toolkit config add-greeting "Salutations"
# Save configuration to file
- my-python-package config save config.json
+ greeting-toolkit config save config.json
# Load configuration from file
- my-python-package config load config.json
+ greeting-toolkit config load config.json
Global Options
------------
@@ -186,16 +186,16 @@ Examples:
.. code-block:: bash
# Show help for the hello command
- my-python-package hello --help
+ greeting-toolkit hello --help
# Show version information
- my-python-package --version
+ greeting-toolkit --version
# Set logging level
- my-python-package hello World --log-level debug
+ greeting-toolkit hello World --log-level debug
# Log to file
- my-python-package hello World --log-file greeting.log
+ greeting-toolkit hello World --log-file greeting.log
Advanced Usage
------------
@@ -211,15 +211,15 @@ You can use the CLI in shell scripts:
# Greet all users in a file
while read name; do
- my-python-package hello "$name" --greeting "Welcome"
+ greeting-toolkit hello "$name" --greeting "Welcome"
done < users.txt
# Save and load configuration
- my-python-package config set --greeting "Hi" --punctuation "!"
- my-python-package config save my_config.json
+ greeting-toolkit config set --greeting "Hi" --punctuation "!"
+ greeting-toolkit config save my_config.json
# Later, restore the configuration
- my-python-package config load my_config.json
+ greeting-toolkit config load my_config.json
Output Redirection
~~~~~~~~~~~~~~~~
@@ -229,10 +229,10 @@ You can redirect the output to files:
.. code-block:: bash
# Save greetings to a file
- my-python-package multi Alice Bob Charlie > greetings.txt
+ greeting-toolkit multi Alice Bob Charlie > greetings.txt
# Append more greetings
- my-python-package hello Dave >> greetings.txt
+ greeting-toolkit hello Dave >> greetings.txt
Error Handling
~~~~~~~~~~~~
@@ -242,6 +242,6 @@ The CLI will return non-zero exit codes on errors:
.. code-block:: bash
# Script example with error handling
- if ! my-python-package hello ""; then
+ if ! greeting-toolkit hello ""; then
echo "Failed to greet empty name"
fi
diff --git a/docs/config.py b/docs/config.py
index 14733c9..ec2f7bf 100644
--- a/docs/config.py
+++ b/docs/config.py
@@ -14,13 +14,13 @@
# -- Project information -----------------------------------------------------
-project = "my_python_package"
+project = "greeting-toolkit"
copyright = f"{datetime.now().year}, Diogo Ribeiro"
author = "Diogo Ribeiro"
# The full version, including alpha/beta/rc tags
try:
- from my_python_package import __version__
+ from greeting_toolkit import __version__
release = __version__
except ImportError:
diff --git a/docs/contributing.rst b/docs/contributing.rst
index fa9f314..3caa761 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -2,7 +2,7 @@
Contributing
============
-Thank you for considering contributing to ``my_python_package``! This page provides guidelines for contributing to the project.
+Thank you for considering contributing to ``greeting_toolkit``! This page provides guidelines for contributing to the project.
Code of Conduct
--------------
diff --git a/docs/development.rst b/docs/development.rst
index 5b56640..38b745b 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -2,7 +2,7 @@
Development Guide
=================
-This guide will help you set up your development environment and understand the workflow for contributing to ``my_python_package``.
+This guide will help you set up your development environment and understand the workflow for contributing to ``greeting_toolkit``.
Development Environment Setup
---------------------------
@@ -21,8 +21,8 @@ Initial Setup
.. code-block:: bash
- git clone https://github.com/DiogoRibeiro7/my_python_package.git
- cd my_python_package
+ git clone https://github.com/DiogoRibeiro7/greeting-toolkit.git
+ cd greeting-toolkit
2. Install dependencies and set up pre-commit hooks:
@@ -143,7 +143,7 @@ Project Structure
.. code-block:: text
- my_python_package/
+ greeting_toolkit/
โโโ pyproject.toml # Project metadata, dependencies
โโโ setup.cfg # Configuration for various tools
โโโ tox.ini # Multi-environment testing
@@ -151,7 +151,7 @@ Project Structure
โโโ .editorconfig # Editor configuration
โโโ Makefile # Common development tasks
โโโ src/
- โ โโโ my_python_package/ # Package source code
+ โ โโโ greeting_toolkit/ # Package source code
โ โโโ __init__.py # Package exports
โ โโโ __main__.py # Module execution entry point
โ โโโ core.py # Core greeting functionality
@@ -284,7 +284,7 @@ To run tests with coverage and see which lines are not covered:
.. code-block:: bash
- pytest --cov=my_python_package --cov-report=term-missing
+ pytest --cov=greeting_toolkit --cov-report=term-missing
Debugging Tips
~~~~~~~~~~~~
diff --git a/docs/index.rst b/docs/index.rst
index bfc0f86..24b6eb6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,23 +1,23 @@
===========================
-my_python_package
+greeting-toolkit
===========================
A minimal but production-ready Python package scaffold configured for publishing to PyPI.
-.. image:: https://img.shields.io/pypi/v/my_python_package.svg
- :target: https://pypi.org/project/my_python_package/
+.. image:: https://img.shields.io/pypi/v/greeting-toolkit.svg
+ :target: https://pypi.org/project/greeting-toolkit/
:alt: PyPI version
-.. image:: https://img.shields.io/pypi/pyversions/my_python_package.svg
- :target: https://pypi.org/project/my_python_package/
+.. image:: https://img.shields.io/pypi/pyversions/greeting-toolkit.svg
+ :target: https://pypi.org/project/greeting-toolkit/
:alt: Python Versions
-.. image:: https://github.com/DiogoRibeiro7/my_python_package/actions/workflows/test.yml/badge.svg
- :target: https://github.com/DiogoRibeiro7/my_python_package/actions/workflows/test.yml
+.. image:: https://github.com/DiogoRibeiro7/greeting-toolkit/actions/workflows/test.yml/badge.svg
+ :target: https://github.com/DiogoRibeiro7/greeting-toolkit/actions/workflows/test.yml
:alt: Tests
.. image:: https://img.shields.io/badge/coverage-95%25-brightgreen
- :target: https://codecov.io/gh/DiogoRibeiro7/my_python_package
+ :target: https://codecov.io/gh/DiogoRibeiro7/greeting-toolkit
:alt: Coverage
.. image:: https://img.shields.io/badge/License-MIT-yellow.svg
@@ -49,7 +49,7 @@ It provides various greeting functions with configurable options for formatting,
.. code-block:: python
- from my_python_package import hello
+ from greeting_toolkit import hello
# Basic usage
greeting = hello("World")
diff --git a/docs/installation.rst b/docs/installation.rst
index 779b4fd..07fae31 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -5,11 +5,11 @@ Installation
From PyPI
---------
-The package is available on `PyPI `_, and you can install it using pip:
+The package is available on `PyPI `_, and you can install it using pip:
.. code-block:: bash
- pip install my_python_package
+ pip install greeting-toolkit
Using Poetry
-----------
@@ -18,7 +18,7 @@ If you use `Poetry `_ for dependency management, you
.. code-block:: bash
- poetry add my_python_package
+ poetry add greeting-toolkit
From Source
----------
@@ -27,8 +27,8 @@ To install the package from source, first clone the repository:
.. code-block:: bash
- git clone https://github.com/DiogoRibeiro7/my_python_package.git
- cd my_python_package
+ git clone https://github.com/DiogoRibeiro7/greeting-toolkit.git
+ cd greeting-toolkit
Then install it using one of the following methods:
@@ -71,7 +71,7 @@ To verify that the package is installed correctly, you can run the following Pyt
.. code-block:: python
- from my_python_package import hello
+ from greeting_toolkit import hello
print(hello("World")) # Should output: Hello, World!
@@ -79,6 +79,6 @@ Or use the command-line interface:
.. code-block:: bash
- my-python-package hello World
+ greeting-toolkit hello World
This should output: ``Hello, World!``
diff --git a/docs/make_api_docs.py b/docs/make_api_docs.py
index be409b8..517c4db 100644
--- a/docs/make_api_docs.py
+++ b/docs/make_api_docs.py
@@ -18,7 +18,7 @@
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
-PACKAGE_NAME = "my_python_package"
+PACKAGE_NAME = "greeting_toolkit"
API_DIR = Path(__file__).parent / "api"
diff --git a/docs/usage.rst b/docs/usage.rst
index 5ea03f4..5827146 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -2,7 +2,7 @@
Usage
=====
-This page demonstrates how to use the various features of ``my_python_package``.
+This page demonstrates how to use the various features of ``greeting_toolkit``.
Basic Greeting
-------------
@@ -11,7 +11,7 @@ The most basic function is ``hello()``, which returns a simple greeting:
.. code-block:: python
- from my_python_package import hello
+ from greeting_toolkit import hello
# Basic usage
greeting = hello("World")
@@ -28,7 +28,7 @@ For more control over the output, use ``format_greeting()``:
.. code-block:: python
- from my_python_package import format_greeting
+ from greeting_toolkit import format_greeting
# Default formatting
print(format_greeting("World")) # Output: Hello, World!
@@ -51,7 +51,7 @@ To greet multiple people at once, use ``create_greeting_list()``:
.. code-block:: python
- from my_python_package import create_greeting_list
+ from greeting_toolkit import create_greeting_list
# Greet multiple people
greetings = create_greeting_list(["Alice", "Bob", "Charlie"])
@@ -70,7 +70,7 @@ The ``generate_greeting()`` function can adjust the greeting based on the time o
.. code-block:: python
- from my_python_package import generate_greeting
+ from greeting_toolkit import generate_greeting
# Time-based greeting (morning/afternoon/evening)
print(generate_greeting("World", time_based=True))
@@ -94,7 +94,7 @@ For variety, use ``random_greeting()`` to get a different greeting each time:
.. code-block:: python
- from my_python_package import random_greeting
+ from greeting_toolkit import random_greeting
# Get a random greeting
print(random_greeting("World")) # Different greeting each time
@@ -106,7 +106,7 @@ To validate names before using them in greetings, use ``validate_name()``:
.. code-block:: python
- from my_python_package import validate_name
+ from greeting_toolkit import validate_name
# Check if a name is valid
valid, error = validate_name("John")
@@ -127,8 +127,8 @@ You can configure default settings for the package:
.. code-block:: python
- from my_python_package.core import set_default_greeting, set_default_punctuation, add_greeting
- from my_python_package.config import config
+ from greeting_toolkit.core import set_default_greeting, set_default_punctuation, add_greeting
+ from greeting_toolkit.config import config
# Set default greeting
set_default_greeting("Howdy")
@@ -159,7 +159,7 @@ You can combine multiple features for more complex behavior:
.. code-block:: python
- from my_python_package import validate_name, format_greeting, hello
+ from greeting_toolkit import validate_name, format_greeting, hello
def greet_user(name, formal=False, uppercase=False):
# First validate the name
@@ -191,7 +191,7 @@ For more advanced use cases, you can create a custom greeting system:
.. code-block:: python
- from my_python_package import format_greeting, random_greeting, generate_greeting
+ from greeting_toolkit import format_greeting, random_greeting, generate_greeting
import random
class GreetingSystem:
diff --git a/examples/usage.py b/examples/usage.py
index c508113..c8ad8ad 100644
--- a/examples/usage.py
+++ b/examples/usage.py
@@ -1,4 +1,4 @@
-from my_python_package import hello
+from greeting_toolkit import hello
def main():
diff --git a/mkdocs.yaml b/mkdocs.yaml
index 67b4e89..23e9e3c 100644
--- a/mkdocs.yaml
+++ b/mkdocs.yaml
@@ -1,10 +1,10 @@
-site_name: my_python_package
+site_name: greeting-toolkit
site_description: A minimal but production-ready Python package scaffold
site_author: Diogo Ribeiro
-site_url: https://my-python-package.readthedocs.io/
+site_url: https://greeting-toolkit.readthedocs.io/
-repo_name: DiogoRibeiro7/my_python_package
-repo_url: https://github.com/DiogoRibeiro7/my_python_package
+repo_name: DiogoRibeiro7/greeting-toolkit
+repo_url: https://github.com/DiogoRibeiro7/greeting-toolkit
edit_uri: edit/main/docs/
theme:
@@ -100,7 +100,7 @@ extra:
- icon: fontawesome/brands/github
link: https://github.com/DiogoRibeiro7
- icon: fontawesome/brands/python
- link: https://pypi.org/project/my-python-package/
+ link: https://pypi.org/project/greeting-toolkit/
nav:
- Home: index.md
diff --git a/pyproject.toml b/pyproject.toml
index 52a1e46..d9d7d5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,14 @@
[tool.poetry]
-name = "my_python_package"
-version = "0.3.1"
+name = "greeting-toolkit"
+version = "0.3.0"
description = "A short description of the package."
authors = ["Diogo Ribeiro "]
license = "MIT"
readme = "README.md"
-packages = [{ include = "my_python_package", from = "src" }]
-repository = "https://github.com/DiogoRibeiro7/my_python_package"
+packages = [{ include = "greeting_toolkit", from = "src" }]
+repository = "https://github.com/DiogoRibeiro7/greeting-toolkit"
+homepage = "https://github.com/DiogoRibeiro7/greeting-toolkit"
+documentation = "https://github.com/DiogoRibeiro7/greeting-toolkit"
keywords = ["python", "package", "template"]
classifiers = [
"Development Status :: 4 - Beta",
@@ -35,6 +37,7 @@ bandit = "^1.7.7"
pytest-mock = "^3.12.0"
pytest-sugar = "^1.0.0"
pre-commit = "^3.6.2"
+types-setuptools = "*"
[tool.poetry.group.docs]
optional = true
@@ -102,15 +105,15 @@ unfixable = ["F401"]
convention = "google"
[tool.ruff.per-file-ignores]
-"tests/*" = ["ANN", "D", "S101"]
+"tests/*" = ["ANN", "D", "S101", "PT011", "F401", "SIM117", "S102", "SIM115"]
"*/__init__.py" = ["F401"]
[tool.ruff.isort]
-known-first-party = ["my_python_package"]
+known-first-party = ["greeting_toolkit"]
[tool.pytest.ini_options]
minversion = "8.0"
-addopts = "--cov=my_python_package --cov-report=term-missing"
+addopts = "--cov=greeting_toolkit --cov-report=term-missing"
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
@@ -121,7 +124,7 @@ markers = [
]
[tool.coverage.run]
-source = ["my_python_package"]
+source = ["greeting_toolkit"]
omit = ["tests/*"]
[tool.coverage.report]
@@ -139,4 +142,4 @@ exclude_dirs = ["tests", "docs"]
skips = ["B101"]
[tool.poetry.scripts]
-my-python-package = "my_python_package.cli:main"
+greeting-toolkit = "greeting_toolkit.cli:main"
diff --git a/requirements.txt b/requirements.txt
index a07f7d5..a55fd40 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,4 @@
# To update, run:
# poetry export -f requirements.txt --without-hashes -o requirements.txt
-my_python_package>=0.3.0
+greeting-toolkit>=0.3.0
diff --git a/scripts/check_docstrings_coverage.py b/scripts/check_docstrings_coverage.py
index 3fa1448..b566243 100644
--- a/scripts/check_docstrings_coverage.py
+++ b/scripts/check_docstrings_coverage.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python
-"""
-Check docstring coverage in the project.
+"""Check docstring coverage in the project.
This script inspects Python modules, classes, and functions to ensure
they have proper docstrings. It reports items that are missing docstrings
@@ -36,8 +35,7 @@ class DocItem(NamedTuple):
def is_public(name: str) -> bool:
- """
- Check if a name is public (not starting with underscore).
+ """Check if a name is public (not starting with underscore).
Args:
name: The name to check
@@ -51,8 +49,7 @@ def is_public(name: str) -> bool:
def should_have_docstring(node: ast.AST, include_all: bool = False) -> bool:
- """
- Determine if a node should have a docstring based on our standards.
+ """Determine if a node should have a docstring based on our standards.
Args:
node: The AST node to check
@@ -81,35 +78,33 @@ def should_have_docstring(node: ast.AST, include_all: bool = False) -> bool:
return False
-def get_docstring(node: ast.AST) -> Optional[str]:
- """
- Extract docstring from an AST node.
+def get_docstring(node: ast.AST) -> str | None:
+ """Extract docstring from an AST node.
Args:
node: The AST node to extract docstring from
Returns:
- The docstring if present, None otherwise
+ The docstring if present, ``None`` otherwise
"""
try:
if not node.body:
return None
first_node = node.body[0]
- if isinstance(first_node, ast.Expr) and isinstance(first_node.value, ast.Str):
if isinstance(first_node, ast.Expr):
- if (
- isinstance(first_node.value, ast.Str)
- or (isinstance(first_node.value, ast.Constant) and isinstance(first_node.value.value, str))
+ if isinstance(first_node.value, ast.Str):
+ return first_node.value.s
+ if isinstance(first_node.value, ast.Constant) and isinstance(
+ first_node.value.value, str
):
- return first_node.value.s if hasattr(first_node.value, "s") else first_node.value.value
+ return first_node.value.value
return None
except (AttributeError, IndexError):
return None
-def check_file_docstrings(file_path: Path, include_all: bool = False) -> List[DocItem]:
- """
- Check docstring coverage for a Python file.
+def check_file_docstrings(file_path: Path, include_all: bool = False) -> list[DocItem]:
+ """Check docstring coverage for a Python file.
Args:
file_path: Path to the Python file
@@ -118,14 +113,14 @@ def check_file_docstrings(file_path: Path, include_all: bool = False) -> List[Do
Returns:
List of DocItem instances for each item that should have a docstring
"""
- with open(file_path, "r", encoding="utf-8") as f:
+ with open(file_path, encoding="utf-8") as f:
try:
tree = ast.parse(f.read(), filename=str(file_path))
except SyntaxError:
print(f"Syntax error in {file_path}")
return []
- results: List[DocItem] = []
+ results: list[DocItem] = []
# Check module docstring
module_has_doc = bool(get_docstring(tree))
@@ -176,10 +171,9 @@ def check_file_docstrings(file_path: Path, include_all: bool = False) -> List[Do
def check_directory_docstrings(
- directory: Path, include_all: bool = False, exclude: Optional[Set[str]] = None
-) -> Tuple[List[DocItem], Dict[str, float]]:
- """
- Check docstring coverage for all Python files in a directory.
+ directory: Path, include_all: bool = False, exclude: set[str] | None = None
+) -> tuple[list[DocItem], dict[str, float]]:
+ """Check docstring coverage for all Python files in a directory.
Args:
directory: Directory to check
@@ -203,7 +197,7 @@ def check_directory_docstrings(
".ruff_cache",
}
- all_results: List[DocItem] = []
+ all_results: list[DocItem] = []
for root, dirs, files in os.walk(directory):
# Skip excluded directories
dirs[:] = [d for d in dirs if d not in exclude]
@@ -215,7 +209,7 @@ def check_directory_docstrings(
all_results.extend(results)
# Calculate statistics
- stats: Dict[str, Dict[str, int]] = {"module": {}, "class": {}, "function": {}, "method": {}}
+ stats: dict[str, dict[str, int]] = {"module": {}, "class": {}, "function": {}, "method": {}}
for item in all_results:
stats.setdefault(item.type, {})
stats[item.type].setdefault("total", 0)
@@ -226,7 +220,7 @@ def check_directory_docstrings(
stats[item.type]["with_docstring"] += 1
# Calculate percentages
- percentages: Dict[str, float] = {}
+ percentages: dict[str, float] = {}
total_items = 0
total_with_docs = 0
@@ -246,10 +240,9 @@ def check_directory_docstrings(
def print_report(
- items: List[DocItem], stats: Dict[str, float], show_documented: bool = False
+ items: list[DocItem], stats: dict[str, float], show_documented: bool = False
) -> None:
- """
- Print a docstring coverage report.
+ """Print a docstring coverage report.
Args:
items: List of DocItem instances
@@ -291,9 +284,7 @@ def print_report(
def main():
"""Run the docstring coverage check."""
parser = ArgumentParser(description="Check docstring coverage")
- parser.add_argument(
- "--dir", type=str, default="src", help="Directory to check (default: src)"
- )
+ parser.add_argument("--dir", type=str, default="src", help="Directory to check (default: src)")
parser.add_argument(
"--include-all",
action="store_true",
diff --git a/scripts/check_imports_vs_pyproject.py b/scripts/check_imports_vs_pyproject.py
index 79af679..a28d50d 100644
--- a/scripts/check_imports_vs_pyproject.py
+++ b/scripts/check_imports_vs_pyproject.py
@@ -1,13 +1,12 @@
#!/usr/bin/env python
-"""
-check_imports_vs_pyproject.py
+"""check_imports_vs_pyproject.py
Scan Python sources for imports, compare to pyproject.toml, and optionally FIX:
- Adds missing dependencies to the chosen group with a selectable spec strategy.
- Preserves formatting/comments using tomlkit.
- Supports Poetry ([tool.poetry]) and PEP 621 ([project]).
-Examples
+Examples:
--------
# Detect only (text)
python scripts/check_imports_vs_pyproject.py
@@ -37,29 +36,31 @@
import sys
import urllib.error
import urllib.request
+from collections.abc import Iterable
from dataclasses import dataclass
from difflib import unified_diff
from pathlib import Path
-from typing import Dict, Iterable, List, Optional, Set, Tuple
+from typing import Dict, List, Optional, Set, Tuple
+from urllib.parse import urlparse
import tomlkit
from packaging.requirements import Requirement
-from packaging.version import Version, InvalidVersion
+from packaging.version import InvalidVersion, Version
# --- Optional helpers for stdlib / env mapping ---
try:
- _STDLIB: Set[str] = set(sys.stdlib_module_names) # py>=3.10
+ _STDLIB: set[str] = set(sys.stdlib_module_names) # py>=3.10
except Exception:
_STDLIB = set()
try:
from importlib.metadata import packages_distributions
- _MODULE_TO_DISTS: Dict[str, List[str]] = packages_distributions()
+ _MODULE_TO_DISTS: dict[str, list[str]] = packages_distributions()
except Exception:
_MODULE_TO_DISTS = {}
-_COMMON_MODULE_TO_DIST: Dict[str, str] = {
+_COMMON_MODULE_TO_DIST: dict[str, str] = {
"bs4": "beautifulsoup4",
"cv2": "opencv-python",
"PIL": "Pillow",
@@ -81,13 +82,13 @@
class Config:
root: Path
pyproject: Path
- groups: List[str] # Which groups count as "declared" when checking
+ groups: list[str] # Which groups count as "declared" when checking
include_optional: bool
fail_on: str # missing|unused|both|none
fmt: str # text|json
- exclude_dirs: List[str]
+ exclude_dirs: list[str]
use_env_map: bool
- src_hints: List[str]
+ src_hints: list[str]
# Fix-related:
fix: bool
fix_to: str # main|dev|
@@ -100,11 +101,11 @@ class Config:
@dataclass
class Report:
- missing: Dict[str, Set[str]] # dist -> {modules}
- unused: Set[str]
- ambiguous: Dict[str, Set[str]]
- used_dists: Set[str]
- declared_dists: Set[str]
+ missing: dict[str, set[str]] # dist -> {modules}
+ unused: set[str]
+ ambiguous: dict[str, set[str]]
+ used_dists: set[str]
+ declared_dists: set[str]
# ---------------- Utilities ----------------
@@ -118,7 +119,7 @@ def is_stdlib(top: str) -> bool:
return top in _STDLIB
-def iter_py_files(root: Path, exclude_dirs: List[str]) -> Iterable[Path]:
+def iter_py_files(root: Path, exclude_dirs: list[str]) -> Iterable[Path]:
default = {
".git",
".hg",
@@ -141,13 +142,13 @@ def iter_py_files(root: Path, exclude_dirs: List[str]) -> Iterable[Path]:
yield p
-def parse_imports(pyfile: Path) -> Set[str]:
+def parse_imports(pyfile: Path) -> set[str]:
text = pyfile.read_text(encoding="utf-8", errors="ignore")
try:
tree = ast.parse(text, filename=str(pyfile))
except SyntaxError:
return set()
- tops: Set[str] = set()
+ tops: set[str] = set()
for n in ast.walk(tree):
if isinstance(n, ast.Import):
for a in n.names:
@@ -161,8 +162,8 @@ def parse_imports(pyfile: Path) -> Set[str]:
return tops
-def discover_local_tops(root: Path, hints: List[str]) -> Set[str]:
- locals_: Set[str] = set()
+def discover_local_tops(root: Path, hints: list[str]) -> set[str]:
+ locals_: set[str] = set()
def scan(base: Path) -> None:
if not base.exists() or not base.is_dir():
@@ -181,7 +182,7 @@ def scan(base: Path) -> None:
return locals_
-def map_module_to_dists(mod: str, use_env: bool = True) -> List[str]:
+def map_module_to_dists(mod: str, use_env: bool = True) -> list[str]:
if use_env and _MODULE_TO_DISTS:
d = _MODULE_TO_DISTS.get(mod, [])
if d:
@@ -194,11 +195,12 @@ def map_module_to_dists(mod: str, use_env: bool = True) -> List[str]:
# ------------- PyPI lookup + strategy -------------
-def fetch_latest(
- name: str, timeout: float, include_prerelease: bool
-) -> Optional[Version]:
+def fetch_latest(name: str, timeout: float, include_prerelease: bool) -> Version | None:
url = f"https://pypi.org/pypi/{pep503(name)}/json"
try:
+ parsed = urlparse(url)
+ if parsed.scheme not in {"http", "https"}:
+ raise ValueError(f"Unsupported URL scheme: {parsed.scheme}")
with urllib.request.urlopen(url, timeout=timeout) as r:
data = json.loads(r.read().decode("utf-8"))
except (
@@ -209,7 +211,7 @@ def fetch_latest(
):
return None
releases = data.get("releases", {}) or {}
- best: Optional[Version] = None
+ best: Version | None = None
for vstr, files in releases.items():
if not files:
continue
@@ -255,7 +257,7 @@ def poetry_spec_for(v: Version, strategy: str) -> str:
# ------------- Read/Write pyproject (tomlkit) -------------
-def load_doc(path: Path) -> Tuple[str, tomlkit.TOMLDocument]:
+def load_doc(path: Path) -> tuple[str, tomlkit.TOMLDocument]:
text = path.read_text(encoding="utf-8")
return text, tomlkit.parse(text)
@@ -269,16 +271,14 @@ def layout(doc: tomlkit.TOMLDocument) -> str:
return "poetry"
if "project" in doc:
return "pep621"
- raise ValueError(
- "Unsupported pyproject: neither [tool.poetry] nor [project] found."
- )
+ raise ValueError("Unsupported pyproject: neither [tool.poetry] nor [project] found.")
def collect_declared(
- doc: tomlkit.TOMLDocument, groups: List[str], include_optional: bool
-) -> Set[str]:
+ doc: tomlkit.TOMLDocument, groups: list[str], include_optional: bool
+) -> set[str]:
wanted = set(groups)
- dec: Set[str] = set()
+ dec: set[str] = set()
# Poetry
tool = doc.get("tool", {})
@@ -325,11 +325,8 @@ def collect_declared(
return dec
-def add_dep_to_doc(
- doc: tomlkit.TOMLDocument, dist: str, spec: str, fix_to: str
-) -> None:
- """
- Add dependency to doc in the selected group.
+def add_dep_to_doc(doc: tomlkit.TOMLDocument, dist: str, spec: str, fix_to: str) -> None:
+ """Add dependency to doc in the selected group.
- Poetry: under [tool.poetry.dependencies] or group..dependencies
- PEP 621: in project.dependencies (main) or optional-dependencies.
"""
@@ -387,14 +384,14 @@ def analyze(cfg: Config) -> Report:
declared = collect_declared(doc, cfg.groups, cfg.include_optional)
local = discover_local_tops(cfg.root, cfg.src_hints)
- imported: Set[str] = set()
+ imported: set[str] = set()
for f in iter_py_files(cfg.root, cfg.exclude_dirs):
imported |= parse_imports(f)
third_party = {m for m in imported if not is_stdlib(m) and m not in local}
- used_dists: Set[str] = set()
- ambiguous: Dict[str, Set[str]] = {}
+ used_dists: set[str] = set()
+ ambiguous: dict[str, set[str]] = {}
for mod in sorted(third_party):
dists = map_module_to_dists(mod, cfg.use_env_map)
if len(dists) == 1:
@@ -406,7 +403,7 @@ def analyze(cfg: Config) -> Report:
if d in declared:
used_dists.add(d)
- missing: Dict[str, Set[str]] = {}
+ missing: dict[str, set[str]] = {}
for mod in sorted(third_party):
dists = map_module_to_dists(mod, cfg.use_env_map)
if any(d in declared for d in dists):
@@ -418,8 +415,7 @@ def analyze(cfg: Config) -> Report:
def apply_fix(cfg: Config, rep: Report) -> int:
- """
- Add missing distributions to pyproject.toml using selected strategy.
+ """Add missing distributions to pyproject.toml using selected strategy.
Returns 0 (write/diff success) or raises on errors.
"""
if not rep.missing:
@@ -454,7 +450,7 @@ def apply_fix(cfg: Config, rep: Report) -> int:
# ------------- CLI -------------
-def parse_args(argv: Optional[List[str]] = None) -> Config:
+def parse_args(argv: list[str] | None = None) -> Config:
p = argparse.ArgumentParser(
description="Check (and optionally fix) imports vs pyproject dependencies."
)
@@ -500,9 +496,7 @@ def parse_args(argv: Optional[List[str]] = None) -> Config:
)
# Fix options
- p.add_argument(
- "--fix", action="store_true", help="Write missing deps into pyproject.toml."
- )
+ p.add_argument("--fix", action="store_true", help="Write missing deps into pyproject.toml.")
p.add_argument(
"--fix-to",
default="main",
@@ -537,9 +531,7 @@ def parse_args(argv: Optional[List[str]] = None) -> Config:
action="store_true",
help="Dry-run: print unified diff; do not write.",
)
- p.add_argument(
- "--timeout", type=float, default=8.0, help="HTTP timeout for PyPI lookups."
- )
+ p.add_argument("--timeout", type=float, default=8.0, help="HTTP timeout for PyPI lookups.")
a = p.parse_args(argv)
groups = [g.strip() for g in a.groups.split(",") if g.strip()]
@@ -566,7 +558,7 @@ def parse_args(argv: Optional[List[str]] = None) -> Config:
def format_report_text(r: Report) -> str:
- lines: List[str] = []
+ lines: list[str] = []
if r.missing:
lines.append("MISSING (imported but not declared):")
for dist, modules in sorted(r.missing.items()):
@@ -600,7 +592,7 @@ def format_report_json(r: Report) -> str:
return json.dumps(obj, indent=2, sort_keys=True)
-def main(argv: Optional[List[str]] = None) -> int:
+def main(argv: list[str] | None = None) -> int:
cfg = parse_args(argv)
rep = analyze(cfg)
diff --git a/scripts/generate_api_docs.py b/scripts/generate_api_docs.py
index 3c3b747..9825733 100644
--- a/scripts/generate_api_docs.py
+++ b/scripts/generate_api_docs.py
@@ -161,7 +161,7 @@ def create_index_file(
structure: Dict[str, List[str]],
output_dir: Path,
format_type: Literal["html", "markdown"] = "markdown",
- package_name: str = "my_python_package"
+ package_name: str = "greeting_toolkit"
) -> None:
"""
Create an index file that links to all the module documentation.
@@ -268,7 +268,7 @@ def create_index_file(
def generate_docs(
format_type: Literal["html", "markdown"] = "html",
output_dir: Optional[Path] = None,
- package_name: str = "my_python_package",
+ package_name: str = "greeting_toolkit",
) -> bool:
"""
Generate documentation using pdoc.
@@ -351,8 +351,8 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--package",
- default="my_python_package",
- help="Package name to document (default: my_python_package)",
+ default="greeting_toolkit",
+ help="Package name to document (default: greeting_toolkit)",
)
return parser.parse_args()
diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py
index a77b9d8..ae329c2 100644
--- a/scripts/generate_docs.py
+++ b/scripts/generate_docs.py
@@ -43,7 +43,7 @@ def check_pdoc_installed() -> bool:
def generate_docs(
format_type: Literal["html", "markdown"] = "html",
output_dir: Optional[Path] = None,
- package_name: str = "my_python_package",
+ package_name: str = "greeting_toolkit",
) -> bool:
"""
Generate documentation using pdoc.
diff --git a/scripts/pyproject_updater.py b/scripts/pyproject_updater.py
index 54a033d..53cfdac 100644
--- a/scripts/pyproject_updater.py
+++ b/scripts/pyproject_updater.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python
-"""
-pyproject_updater.py
+"""pyproject_updater.py
Update dependency constraints in `pyproject.toml` to the **latest** versions from PyPI.
@@ -37,30 +36,33 @@
import sys
import urllib.error
import urllib.request
+from collections.abc import Iterable
from dataclasses import dataclass
from difflib import unified_diff
from pathlib import Path
-from typing import Dict, Iterable, List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple
+from urllib.parse import urlparse
import tomlkit
from packaging.requirements import Requirement
-from packaging.version import Version, InvalidVersion
+from packaging.version import InvalidVersion, Version
@dataclass(frozen=True)
class Options:
- strategy: str # one of: exact, caret, tilde, floor
- allow_major: bool # if False, keep within current major
- include_prerelease: bool # if True, accept pre-releases
- groups: List[str] # e.g., ["main", "dev"]
- only: Optional[List[str]] # package name filters (normalized)
- check: bool # dry-run
- file: Path # pyproject.toml path
- timeout: float # HTTP timeout
+ strategy: str # one of: exact, caret, tilde, floor
+ allow_major: bool # if False, keep within current major
+ include_prerelease: bool # if True, accept pre-releases
+ groups: list[str] # e.g., ["main", "dev"]
+ only: list[str] | None # package name filters (normalized)
+ check: bool # dry-run
+ file: Path # pyproject.toml path
+ timeout: float # HTTP timeout
# ---------- TOML helpers ----------
+
def _read_doc(path: Path):
text = path.read_text(encoding="utf-8")
return text, tomlkit.parse(text)
@@ -68,8 +70,14 @@ def _read_doc(path: Path):
def _write_or_diff(path: Path, before: str, after: str, check: bool) -> int:
if check:
- diff = "".join(unified_diff(before.splitlines(True), after.splitlines(True),
- fromfile=str(path), tofile=str(path)))
+ diff = "".join(
+ unified_diff(
+ before.splitlines(True),
+ after.splitlines(True),
+ fromfile=str(path),
+ tofile=str(path),
+ )
+ )
sys.stdout.write(diff)
return 0
path.write_text(after, encoding="utf-8")
@@ -87,28 +95,28 @@ def _layout(doc) -> str:
# ---------- PyPI version lookup ----------
+
def _normalize_pkg_name(name: str) -> str:
- """
- Normalize per PEP 503 for PyPI URLs: lowercase and replace `_`/`.` with `-`.
+ """Normalize per PEP 503 for PyPI URLs: lowercase and replace `_`/`.` with `-`.
Keep extras separate (e.g., 'foo[bar]') โ we strip extras for lookup.
"""
base = name.split("[", 1)[0]
return base.lower().replace("_", "-").replace(".", "-")
-def _fetch_pypi_versions(name: str, timeout: float) -> Dict[str, bool]:
- """
- Return {version_str: is_yanked=False/True} for package 'name' from PyPI.
- On failure, return empty dict.
- """
+def _fetch_pypi_versions(name: str, timeout: float) -> dict[str, bool]:
+ """Return ``{version_str: is_yanked}`` for package *name* from PyPI."""
url = f"https://pypi.org/pypi/{_normalize_pkg_name(name)}/json"
try:
+ parsed = urlparse(url)
+ if parsed.scheme not in {"http", "https"}:
+ raise ValueError(f"Unsupported URL scheme: {parsed.scheme}")
with urllib.request.urlopen(url, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, json.JSONDecodeError):
return {}
- versions: Dict[str, bool] = {}
+ versions: dict[str, bool] = {}
releases = data.get("releases", {}) or {}
for ver_str, files in releases.items():
# files is a list of distributions; consider version yanked if all files are yanked
@@ -119,11 +127,9 @@ def _fetch_pypi_versions(name: str, timeout: float) -> Dict[str, bool]:
return versions
-def _select_latest_version(versions: Dict[str, bool], include_prerelease: bool) -> Optional[Version]:
- """
- Pick the highest non-yanked Version. If include_prerelease=False, prefer finals.
- """
- valid: List[Version] = []
+def _select_latest_version(versions: dict[str, bool], include_prerelease: bool) -> Version | None:
+ """Pick the highest non-yanked Version. If include_prerelease=False, prefer finals."""
+ valid: list[Version] = []
for ver_str, not_yanked in versions.items():
if not not_yanked:
continue
@@ -141,6 +147,7 @@ def _select_latest_version(versions: Dict[str, bool], include_prerelease: bool)
# ---------- Constraint mapping ----------
+
def _poetry_string_for_strategy(v: Version, strategy: str) -> str:
if strategy == "exact":
return f"=={v}"
@@ -169,9 +176,8 @@ def _pep440_string_for_strategy(v: Version, strategy: str) -> str:
raise ValueError(f"Unknown strategy: {strategy}")
-def _respect_major_allowed(current_spec: Optional[str], latest: Version, allow_major: bool) -> bool:
- """
- If allow_major is False and current_spec indicates a major cap, avoid bumping across majors.
+def _respect_major_allowed(current_spec: str | None, latest: Version, allow_major: bool) -> bool:
+ """If allow_major is False and current_spec indicates a major cap, avoid bumping across majors.
Heuristic: extract existing max major from spec if present; otherwise compare against any pinned/ranged major.
"""
if allow_major:
@@ -180,7 +186,11 @@ def _respect_major_allowed(current_spec: Optional[str], latest: Version, allow_m
return latest.major == latest.major # trivial True
# Try to parse the requirement to see any existing max
try:
- req = Requirement(f"pkg {current_spec}") if " " in current_spec else Requirement(f"pkg {current_spec}")
+ req = (
+ Requirement(f"pkg {current_spec}")
+ if " " in current_spec
+ else Requirement(f"pkg {current_spec}")
+ )
except Exception:
# Fallback: if spec starts with ^ or ~ (Poetry), infer major from latest string of spec if present
if current_spec.startswith("^") or current_spec.startswith("~"):
@@ -206,13 +216,14 @@ def _respect_major_allowed(current_spec: Optional[str], latest: Version, allow_m
# ---------- Dependency iteration & rewriting ----------
+
@dataclass
class DepRef:
- layout: str # "poetry" or "pep621"
- group: str # "main" or group name
- name: str # raw name as in file (may include extras)
- current_spec: Optional[str] # string spec; None if path/git or table
- location: Tuple # references for writing (table/array and key/index)
+ layout: str # "poetry" or "pep621"
+ group: str # "main" or group name
+ name: str # raw name as in file (may include extras)
+ current_spec: str | None # string spec; None if path/git or table
+ location: tuple # references for writing (table/array and key/index)
def _iter_poetry_deps(doc, groups: Iterable[str]) -> Iterable[DepRef]:
@@ -281,8 +292,7 @@ def emit_from_array(arr, group: str):
if not groups_set or "main" in groups_set:
arr = project.setdefault("dependencies", tomlkit.array())
emit = list(emit_from_array(arr, "main"))
- for d in emit:
- yield d
+ yield from emit
# optional groups
opt = project.setdefault("optional-dependencies", tomlkit.table())
@@ -291,14 +301,11 @@ def emit_from_array(arr, group: str):
if groups_set and gname not in groups_set:
continue
emit = list(emit_from_array(arr, gname))
- for d in emit:
- yield d
+ yield from emit
def _set_dep_spec(dep: DepRef, new_spec: str):
- """
- Write back new spec to the TOML document at the stored location.
- """
+ """Write back new spec to the TOML document at the stored location."""
if dep.layout == "poetry":
tbl, key = dep.location # type: ignore[assignment]
if isinstance(tbl, dict):
@@ -321,13 +328,14 @@ def _set_dep_spec(dep: DepRef, new_spec: str):
# ---------- Main upgrade routine ----------
+
def upgrade(pyproject: Path, opts: Options) -> int:
before_text, doc = _read_doc(pyproject)
layout = _layout(doc)
# Which groups to consider by default
groups = opts.groups or ["main", "dev"] # include Poetry dev by default
- only_norm = set(_normalize_pkg_name(n) for n in (opts.only or []))
+ only_norm = {_normalize_pkg_name(n) for n in (opts.only or [])}
# Iterate deps
iterator = _iter_poetry_deps if layout == "poetry" else _iter_pep621_deps
@@ -360,7 +368,13 @@ def upgrade(pyproject: Path, opts: Options) -> int:
if target_major is not None:
# pick highest < target_major+1.0.0
candidates = [Version(v) for v, ok in versions.items() if ok]
- within = [v for v in candidates if (v.major == target_major and (opts.include_prerelease or not v.is_prerelease))]
+ within = [
+ v
+ for v in candidates
+ if (
+ v.major == target_major and (opts.include_prerelease or not v.is_prerelease)
+ )
+ ]
if within:
latest = max(within)
@@ -384,22 +398,51 @@ def upgrade(pyproject: Path, opts: Options) -> int:
return _write_or_diff(pyproject, before_text, after_text, opts.check)
-def parse_args(argv: Optional[List[str]] = None) -> Options:
- p = argparse.ArgumentParser(description="Upgrade pyproject dependency constraints to latest from PyPI.")
+def parse_args(argv: list[str] | None = None) -> Options:
+ p = argparse.ArgumentParser(
+ description="Upgrade pyproject dependency constraints to latest from PyPI."
+ )
p.add_argument("--file", default="pyproject.toml", help="Path to pyproject.toml")
- p.add_argument("--strategy", choices=["exact", "caret", "tilde", "floor"], default="caret",
- help="How to express the updated constraint.")
- p.add_argument("--allow-major", action="store_true", help="Allow bumping to a new MAJOR version.")
- p.add_argument("--respect-major", dest="allow_major", action="store_false",
- help="(default) Keep within the current major if possible.")
+ p.add_argument(
+ "--strategy",
+ choices=["exact", "caret", "tilde", "floor"],
+ default="caret",
+ help="How to express the updated constraint.",
+ )
+ p.add_argument(
+ "--allow-major", action="store_true", help="Allow bumping to a new MAJOR version."
+ )
+ p.add_argument(
+ "--respect-major",
+ dest="allow_major",
+ action="store_false",
+ help="(default) Keep within the current major if possible.",
+ )
p.set_defaults(allow_major=False)
- p.add_argument("--pre", "--include-prerelease", dest="include_prerelease", action="store_true",
- help="Allow pre-releases when picking the latest.")
- p.add_argument("--no-prerelease", dest="include_prerelease", action="store_false",
- help="(default) Exclude pre-releases.")
+ p.add_argument(
+ "--pre",
+ "--include-prerelease",
+ dest="include_prerelease",
+ action="store_true",
+ help="Allow pre-releases when picking the latest.",
+ )
+ p.add_argument(
+ "--no-prerelease",
+ dest="include_prerelease",
+ action="store_false",
+ help="(default) Exclude pre-releases.",
+ )
p.set_defaults(include_prerelease=False)
- p.add_argument("--groups", default="main,dev", help="Comma-separated groups: e.g., main,dev or analytics,docs")
- p.add_argument("--only", default="", help="Comma-separated package names to update (normalized). Empty=all.")
+ p.add_argument(
+ "--groups",
+ default="main,dev",
+ help="Comma-separated groups: e.g., main,dev or analytics,docs",
+ )
+ p.add_argument(
+ "--only",
+ default="",
+ help="Comma-separated package names to update (normalized). Empty=all.",
+ )
p.add_argument("--check", action="store_true", help="Dry-run: show unified diff, do not write.")
p.add_argument("--timeout", type=float, default=8.0, help="HTTP timeout (seconds).")
@@ -419,7 +462,7 @@ def parse_args(argv: Optional[List[str]] = None) -> Options:
)
-def main(argv: Optional[List[str]] = None) -> int:
+def main(argv: list[str] | None = None) -> int:
opts = parse_args(argv)
return upgrade(opts.file, opts)
diff --git a/scripts/rename_package.py b/scripts/rename_package.py
new file mode 100644
index 0000000..63e0420
--- /dev/null
+++ b/scripts/rename_package.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""Rename the project package.
+
+This helper script updates common project files when renaming a Python package.
+It performs a simple search and replace on text files and renames the source
+package directory.
+
+Usage:
+ python scripts/rename_package.py old_name new_name
+
+Both ``old_name`` and ``new_name`` should be given using the distribution name
+form (hyphens allowed). The script will automatically convert them to module
+names by replacing hyphens with underscores when updating imports and source
+folders.
+"""
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+from typing import Iterable
+
+TEXT_EXTENSIONS = {
+ ".py",
+ ".toml",
+ ".md",
+ ".rst",
+ ".txt",
+ ".yaml",
+ ".yml",
+ ".ini",
+ ".cfg",
+ ".json",
+ "Makefile",
+ "Dockerfile",
+ "",
+}
+
+
+def iter_text_files(root: Path) -> Iterable[Path]:
+ """Yield text files under ``root`` excluding the ``.git`` directory."""
+ for path in root.rglob("*"):
+ if path.is_dir() or ".git" in path.parts:
+ continue
+ ext = path.suffix
+ if path.name in TEXT_EXTENSIONS or ext in TEXT_EXTENSIONS:
+ try:
+ path.read_text()
+ except UnicodeDecodeError:
+ continue
+ yield path
+
+
+def replace_in_file(path: Path, replacements: dict[str, str]) -> None:
+ content = path.read_text()
+ new_content = content
+ for old, new in replacements.items():
+ new_content = new_content.replace(old, new)
+ if new_content != content:
+ path.write_text(new_content)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Rename Python package in project")
+ parser.add_argument("old_name", help="Current package name (distribution form)")
+ parser.add_argument("new_name", help="New package name (distribution form)")
+ args = parser.parse_args()
+
+ old_dist = args.old_name
+ new_dist = args.new_name
+ old_mod = old_dist.replace("-", "_")
+ new_mod = new_dist.replace("-", "_")
+ old_cli = old_mod.replace("_", "-")
+ new_cli = new_mod.replace("_", "-")
+
+ project_root = Path(__file__).resolve().parent.parent
+
+ # Rename source directory
+ src = project_root / "src"
+ old_pkg_dir = src / old_mod
+ new_pkg_dir = src / new_mod
+ if old_pkg_dir.exists() and not new_pkg_dir.exists():
+ old_pkg_dir.rename(new_pkg_dir)
+
+ replacements = {
+ old_dist: new_dist,
+ old_mod: new_mod,
+ old_cli: new_cli,
+ }
+
+ for file in iter_text_files(project_root):
+ replace_in_file(file, replacements)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_package.py b/scripts/test_package.py
new file mode 100644
index 0000000..c21f1c2
--- /dev/null
+++ b/scripts/test_package.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+"""Run a basic verification of the greeting-toolkit package.
+
+This script performs a series of smoke tests to ensure the package works
+correctly after renaming:
+
+1. Install the project in editable mode.
+2. Import the ``greeting_toolkit`` module.
+3. Execute a simple CLI command.
+4. Run the test suite.
+5. Build the documentation to ensure docs generation succeeds.
+
+The documentation is generated into a temporary directory so no repository
+files are modified.
+"""
+from __future__ import annotations
+
+import os
+import subprocess # noqa: S404 # nosec B404
+import sys
+import tempfile
+from pathlib import Path
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+
+
+def run(cmd: list[str], **kwargs: object) -> None:
+ """Execute ``cmd`` safely and stream its output."""
+ if not all(isinstance(part, str) for part in cmd):
+ raise TypeError("Command parts must be strings")
+ sys.stdout.write(f"$ {' '.join(cmd)}\n")
+ subprocess.run(cmd, check=True, **kwargs) # noqa: S603 # nosec B603
+
+
+def main() -> int:
+ """Run verification steps and return exit code."""
+ run([sys.executable, "-m", "pip", "install", "-e", str(PROJECT_ROOT)])
+ run([sys.executable, "-c", "import greeting_toolkit"])
+ run(["greeting-toolkit", "hello", "World"])
+
+ env = os.environ.copy()
+ env["PYTHONPATH"] = str(PROJECT_ROOT / "src")
+ run([sys.executable, "-m", "pytest", "--no-cov"], env=env)
+
+ # Ensure documentation can be generated with a known version
+ run([sys.executable, "-m", "pip", "install", "pdoc==14.3.0"])
+ with tempfile.TemporaryDirectory() as tmpdir:
+ run([sys.executable, "-m", "pdoc", "greeting_toolkit", "-o", tmpdir])
+
+ sys.stdout.write("All checks completed successfully.\n")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/setup.cfg b/setup.cfg
index f31143a..171d6a2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,7 +19,7 @@ filterwarnings =
ignore::PendingDeprecationWarning
[coverage:run]
-source = my_python_package
+source = greeting_toolkit
omit = tests/*
[coverage:report]
diff --git a/src/my_python_package/__init__.py b/src/greeting_toolkit/__init__.py
similarity index 75%
rename from src/my_python_package/__init__.py
rename to src/greeting_toolkit/__init__.py
index 136d421..66fceb2 100644
--- a/src/my_python_package/__init__.py
+++ b/src/greeting_toolkit/__init__.py
@@ -1,5 +1,4 @@
-"""
-my_python_package - A minimal but production-ready Python package.
+"""greeting_toolkit - A minimal but production-ready Python package.
This package demonstrates a properly structured Python project
with modern tooling and configuration.
@@ -8,18 +7,18 @@
options for formatting, randomization, and validation.
Examples:
- >>> from my_python_package import hello
+ >>> from greeting_toolkit import hello
>>> hello("World")
'Hello, World!'
- >>> from my_python_package import generate_greeting
+ >>> from greeting_toolkit import generate_greeting
>>> import re
>>> # Time-based greeting will vary by time of day
>>> bool(re.match(r'(Good (morning|afternoon|evening)|Hello), World!',
... generate_greeting("World", time_based=True)))
True
- >>> from my_python_package import random_greeting
+ >>> from greeting_toolkit import random_greeting
>>> # Random greeting will contain the name
>>> "World" in random_greeting("World")
True
@@ -36,7 +35,7 @@
)
# Version and author information
-__version__: str = "0.2.0"
+__version__: str = "0.3.0"
__author__: str = "Diogo Ribeiro"
# Public API
@@ -49,16 +48,18 @@
"format_greeting",
]
-# Enable CLI usage with python -m my_python_package
+
+# Enable CLI usage with python -m greeting_toolkit
def _main() -> None:
- """
- Entry point for module execution.
+ """Entry point for module execution.
This function is called when the module is run directly
- with `python -m my_python_package`.
+ with `python -m greeting_toolkit`.
"""
- from .cli import main
import sys
+
+ from .cli import main
+
sys.exit(main())
diff --git a/src/my_python_package/__main__.py b/src/greeting_toolkit/__main__.py
similarity index 100%
rename from src/my_python_package/__main__.py
rename to src/greeting_toolkit/__main__.py
diff --git a/src/my_python_package/cli.py b/src/greeting_toolkit/cli.py
similarity index 99%
rename from src/my_python_package/cli.py
rename to src/greeting_toolkit/cli.py
index 6c24df3..60fb8e2 100644
--- a/src/my_python_package/cli.py
+++ b/src/greeting_toolkit/cli.py
@@ -1,4 +1,4 @@
-"""Command-line interface for my_python_package."""
+"""Command-line interface for greeting_toolkit."""
import argparse
import json
@@ -25,7 +25,7 @@
def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
- prog="my-python-package",
+ prog="greeting-toolkit",
description="A simple greeting package",
)
diff --git a/src/my_python_package/config.py b/src/greeting_toolkit/config.py
similarity index 83%
rename from src/my_python_package/config.py
rename to src/greeting_toolkit/config.py
index 5f964c1..315ff30 100644
--- a/src/my_python_package/config.py
+++ b/src/greeting_toolkit/config.py
@@ -1,17 +1,24 @@
-"""Configuration module for my_python_package."""
+"""Configuration module for greeting_toolkit."""
import json
import os
+from contextlib import suppress
from pathlib import Path
-from typing import Any, Dict, List, Optional, Union, cast
+from typing import Any, cast
# Default configuration
-DEFAULT_CONFIG: Dict[str, Any] = {
+DEFAULT_CONFIG: dict[str, Any] = {
"default_greeting": "Hello",
"default_punctuation": "!",
"available_greetings": [
- "Hello", "Hi", "Hey", "Greetings", "Hey there",
- "Howdy", "Welcome", "Good to see you"
+ "Hello",
+ "Hi",
+ "Hey",
+ "Greetings",
+ "Hey there",
+ "Howdy",
+ "Welcome",
+ "Good to see you",
],
"max_name_length": 50,
"formal_title": "Mr./Ms. ",
@@ -19,8 +26,7 @@
class Config:
- """
- Configuration handler for my_python_package.
+ """Configuration handler for greeting_toolkit.
Manages package configuration with defaults and optional loading from file.
Configuration can be saved to and loaded from JSON files.
@@ -44,9 +50,8 @@ class Config:
True
"""
- def __init__(self, config_path: Optional[Path] = None) -> None:
- """
- Initialize configuration.
+ def __init__(self, config_path: Path | None = None) -> None:
+ """Initialize configuration.
Args:
config_path: Path to custom config file
@@ -70,39 +75,35 @@ def __init__(self, config_path: Optional[Path] = None) -> None:
>>> import os
>>> os.unlink(tmp_path)
"""
- self._config: Dict[str, Any] = DEFAULT_CONFIG.copy()
- self._config_path: Optional[Path] = config_path
+ self._config: dict[str, Any] = DEFAULT_CONFIG.copy()
+ self._config_path: Path | None = config_path
self._load_config()
def _load_config(self) -> None:
- """
- Load configuration from file if exists.
+ """Load configuration from file if exists.
Checks for a configuration file path either from initialization or
- from the MY_PYTHON_PACKAGE_CONFIG environment variable. If a valid
+ from the GREETING_TOOLKIT_CONFIG environment variable. If a valid
JSON file is found, its values are merged with the defaults.
"""
# Check environment variable first
- env_config = os.environ.get("MY_PYTHON_PACKAGE_CONFIG")
+ env_config = os.environ.get("GREETING_TOOLKIT_CONFIG")
if env_config:
- try:
+ with suppress(Exception):
self._config_path = Path(env_config)
- except Exception:
- pass
# Try loading from file
if self._config_path and self._config_path.exists():
try:
- with open(self._config_path, "r") as f:
+ with open(self._config_path) as f:
user_config = json.load(f)
self._config.update(user_config)
- except (json.JSONDecodeError, IOError, OSError):
+ except (json.JSONDecodeError, OSError):
# Fall back to defaults on error
pass
- def save_config(self, path: Optional[Path] = None) -> None:
- """
- Save current configuration to file.
+ def save_config(self, path: Path | None = None) -> None:
+ """Save current configuration to file.
Args:
path: Path to save config (defaults to current config path)
@@ -132,15 +133,14 @@ def save_config(self, path: Optional[Path] = None) -> None:
... saved_data["default_greeting"] == "Bonjour"
True
"""
- save_path: Optional[Path] = path or self._config_path
+ save_path: Path | None = path or self._config_path
if save_path:
with open(save_path, "w") as f:
json.dump(self._config, f, indent=2)
@property
def default_greeting(self) -> str:
- """
- Get default greeting.
+ """Get default greeting.
Returns:
The configured default greeting
@@ -154,8 +154,7 @@ def default_greeting(self) -> str:
@default_greeting.setter
def default_greeting(self, value: str) -> None:
- """
- Set default greeting.
+ """Set default greeting.
Args:
value: New default greeting
@@ -173,8 +172,7 @@ def default_greeting(self, value: str) -> None:
@property
def default_punctuation(self) -> str:
- """
- Get default punctuation.
+ """Get default punctuation.
Returns:
The configured default punctuation
@@ -188,8 +186,7 @@ def default_punctuation(self) -> str:
@default_punctuation.setter
def default_punctuation(self, value: str) -> None:
- """
- Set default punctuation.
+ """Set default punctuation.
Args:
value: New default punctuation
@@ -206,9 +203,8 @@ def default_punctuation(self, value: str) -> None:
self._config["default_punctuation"] = value
@property
- def available_greetings(self) -> List[str]:
- """
- Get available greetings for random selection.
+ def available_greetings(self) -> list[str]:
+ """Get available greetings for random selection.
Returns:
List of greeting strings
@@ -223,12 +219,11 @@ def available_greetings(self) -> List[str]:
>>> all(isinstance(g, str) for g in greetings)
True
"""
- return cast(List[str], self._config["available_greetings"])
+ return cast(list[str], self._config["available_greetings"])
@available_greetings.setter
- def available_greetings(self, value: List[str]) -> None:
- """
- Set available greetings.
+ def available_greetings(self, value: list[str]) -> None:
+ """Set available greetings.
Args:
value: List of greeting strings
@@ -258,8 +253,7 @@ def available_greetings(self, value: List[str]) -> None:
@property
def max_name_length(self) -> int:
- """
- Get maximum name length.
+ """Get maximum name length.
Returns:
Maximum allowed length for names
@@ -275,8 +269,7 @@ def max_name_length(self) -> int:
@max_name_length.setter
def max_name_length(self, value: int) -> None:
- """
- Set maximum name length.
+ """Set maximum name length.
Args:
value: New maximum length
@@ -309,8 +302,7 @@ def max_name_length(self, value: int) -> None:
@property
def formal_title(self) -> str:
- """
- Get formal title prefix.
+ """Get formal title prefix.
Returns:
The configured formal title prefix
@@ -324,8 +316,7 @@ def formal_title(self) -> str:
@formal_title.setter
def formal_title(self, value: str) -> None:
- """
- Set formal title prefix.
+ """Set formal title prefix.
Args:
value: New formal title prefix
@@ -341,9 +332,8 @@ def formal_title(self, value: str) -> None:
"""
self._config["formal_title"] = value
- def as_dict(self) -> Dict[str, Any]:
- """
- Get configuration as dictionary.
+ def as_dict(self) -> dict[str, Any]:
+ """Get configuration as dictionary.
Returns:
A copy of the current configuration dictionary
diff --git a/src/my_python_package/core.py b/src/greeting_toolkit/core.py
similarity index 88%
rename from src/my_python_package/core.py
rename to src/greeting_toolkit/core.py
index db2fbd4..00f0de0 100644
--- a/src/my_python_package/core.py
+++ b/src/greeting_toolkit/core.py
@@ -1,18 +1,17 @@
-"""Core functionality of my_python_package."""
+"""Core functionality of greeting_toolkit."""
from __future__ import annotations
-import random
import re
+import secrets
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple, Union, cast
from .config import config
-def hello(name: str, greeting: Optional[str] = None) -> str:
- """
- Return a personalized greeting message.
+def hello(name: str, greeting: str | None = None) -> str:
+ """Return a personalized greeting message.
Args:
name: The name to greet
@@ -59,8 +58,7 @@ def hello(name: str, greeting: Optional[str] = None) -> str:
def generate_greeting(name: str, formal: bool = False, time_based: bool = False) -> str:
- """
- Generate a context-aware greeting.
+ """Generate a context-aware greeting.
Args:
name: The name to greet
@@ -106,9 +104,8 @@ def generate_greeting(name: str, formal: bool = False, time_based: bool = False)
return f"{greeting}, {title}{name}!"
-def validate_name(name: str) -> Tuple[bool, Optional[str]]:
- """
- Validate a name according to basic rules.
+def validate_name(name: str) -> tuple[bool, str | None]:
+ """Validate a name according to basic rules.
Rules:
- Must not be empty
@@ -174,9 +171,8 @@ def validate_name(name: str) -> Tuple[bool, Optional[str]]:
return True, None
-def create_greeting_list(names: List[str], greeting: Optional[str] = None) -> List[str]:
- """
- Create a list of greetings for multiple names.
+def create_greeting_list(names: list[str], greeting: str | None = None) -> list[str]:
+ """Create a list of greetings for multiple names.
Args:
names: List of names to greet
@@ -211,8 +207,10 @@ def create_greeting_list(names: List[str], greeting: Optional[str] = None) -> Li
def random_greeting(name: str) -> str:
- """
- Generate a random greeting from a predefined list.
+ """Generate a random greeting from a predefined list.
+
+ Uses :mod:`secrets` for unpredictability; while this is unnecessary for
+ most greetings, it avoids the weaker :mod:`random` module.
Args:
name: The name to greet
@@ -221,39 +219,29 @@ def random_greeting(name: str) -> str:
A random greeting message
Examples:
- >>> # Control randomness with seed
- >>> import random
- >>> random.seed(42) # Set seed for reproducible example
- >>> # The exact greeting depends on config.available_greetings
>>> greeting = random_greeting("Python")
- >>> "Python" in greeting # Name should be in the greeting
+ >>> "Python" in greeting
True
>>> any(g in greeting for g in config.available_greetings)
True
- >>> # Verify different calls give different results
- >>> # Reset the seed
- >>> random.seed(None)
- >>> # Get multiple greetings
>>> greetings = [random_greeting("Test") for _ in range(10)]
- >>> # Count unique greetings - should be more than 1 if truly random
>>> len(set(greetings)) > 1
True
"""
greetings = config.available_greetings
- return f"{random.choice(greetings)}, {name}!"
+ return f"{secrets.choice(greetings)}, {name}!"
def format_greeting(
name: str,
*,
- greeting: Optional[str] = None,
- punctuation: Optional[str] = None,
+ greeting: str | None = None,
+ punctuation: str | None = None,
uppercase: bool = False,
- max_length: Optional[int] = None,
+ max_length: int | None = None,
) -> str:
- """
- Format a greeting with various options.
+ """Format a greeting with various options.
Args:
name: The name to greet
@@ -311,7 +299,7 @@ def format_greeting(
# Truncate to max_length-3, then add "..."
# For "Hello, John!" with max_length=10:
# Take "Hello, J" (8 chars) + "..." = "Hello, J..."
- truncated_base = result[:max_length - 3]
+ truncated_base = result[: max_length - 3]
result = truncated_base + "..."
# Apply uppercase after truncation
@@ -322,8 +310,7 @@ def format_greeting(
def set_default_greeting(greeting: str) -> None:
- """
- Set the default greeting in the configuration.
+ """Set the default greeting in the configuration.
Args:
greeting: New default greeting
@@ -402,7 +389,7 @@ def add_greeting(greeting: str) -> None:
config.available_greetings = greetings
-def get_config() -> Dict[str, Any]:
+def get_config() -> dict[str, Any]:
"""Get the current configuration.
Returns:
@@ -427,4 +414,3 @@ def get_config() -> Dict[str, Any]:
True
"""
return config.as_dict()
-
diff --git a/src/my_python_package/logging.py b/src/greeting_toolkit/logging.py
similarity index 66%
rename from src/my_python_package/logging.py
rename to src/greeting_toolkit/logging.py
index 7c6b7b5..0325c10 100644
--- a/src/my_python_package/logging.py
+++ b/src/greeting_toolkit/logging.py
@@ -1,64 +1,63 @@
-"""Logging configuration for my_python_package."""
+"""Logging configuration for greeting_toolkit."""
import logging
import os
import sys
from pathlib import Path
-from typing import Literal, Optional, Union, overload
+from typing import Literal, overload
# Default log format
DEFAULT_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Package logger
-logger: logging.Logger = logging.getLogger("my_python_package")
+logger: logging.Logger = logging.getLogger("greeting_toolkit")
# Type alias for log levels
-LogLevel = Union[int, str, Literal["debug", "info", "warning", "error", "critical"]]
+LogLevel = int | str | Literal["debug", "info", "warning", "error", "critical"]
@overload
def configure_logging(
level: LogLevel = logging.INFO,
- format_str: Optional[str] = None,
+ format_str: str | None = None,
log_file: None = None,
propagate: bool = False,
-) -> None:
- ...
+) -> None: ...
@overload
def configure_logging(
level: LogLevel = logging.INFO,
- format_str: Optional[str] = None,
- log_file: Union[str, Path] = ...,
+ format_str: str | None = None,
+ log_file: str | Path = ...,
propagate: bool = False,
-) -> None:
- ...
+) -> None: ...
def configure_logging(
level: LogLevel = logging.INFO,
- format_str: Optional[str] = None,
- log_file: Optional[Union[str, Path]] = None,
+ format_str: str | None = None,
+ log_file: str | Path | None = None,
propagate: bool = False,
) -> None:
- """
- Configure logging for the package.
+ """Configure logging for the package.
Args:
level: Logging level (default: INFO)
Can be an integer level or string name like "debug", "info", etc.
format_str: Log format string (default: DEFAULT_FORMAT)
Uses standard Python logging format strings
- log_file: Optional path to log file
+ log_file: Optional path to log file within current working directory
If provided, logs will be written to this file in addition to console
+ The path is resolved and must be located inside the current working
+ directory to prevent writing to unexpected locations
propagate: Whether to propagate to parent loggers
When True, logs will also be sent to parent loggers
Examples:
>>> import tempfile
- >>> with tempfile.NamedTemporaryFile() as temp:
- ... # Configure with INFO level and a log file
+ >>> with tempfile.NamedTemporaryFile(dir=".") as temp:
+ ... # Configure with INFO level and a log file within CWD
... configure_logging(level="info", log_file=temp.name)
... # Get the configured level (20 = INFO)
... logger.level == logging.INFO
@@ -96,21 +95,23 @@ def configure_logging(
# Add file handler if specified
if log_file:
try:
- file_path = Path(log_file)
- # Create directory if it doesn't exist
+ file_path = Path(log_file).expanduser().resolve()
+ cwd = Path.cwd().resolve()
+ if not file_path.is_relative_to(cwd):
+ raise ValueError("log_file must be within the current working directory")
+
if not file_path.parent.exists():
file_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(file_path)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
- except (IOError, OSError) as e:
+ except (OSError, ValueError) as e:
logger.warning(f"Failed to configure log file: {e}")
def get_logger(name: str) -> logging.Logger:
- """
- Get a logger for a specific module.
+ """Get a logger for a specific module.
Args:
name: Module name (relative to package)
@@ -122,27 +123,26 @@ def get_logger(name: str) -> logging.Logger:
>>> # Get a logger for a module
>>> module_logger = get_logger("core")
>>> module_logger.name
- 'my_python_package.core'
+ 'greeting_toolkit.core'
>>> # Get a logger for a nested module
>>> nested_logger = get_logger("utils.helpers")
>>> nested_logger.name
- 'my_python_package.utils.helpers'
+ 'greeting_toolkit.utils.helpers'
"""
- return logging.getLogger(f"my_python_package.{name}")
+ return logging.getLogger(f"greeting_toolkit.{name}")
# Configure from environment variables if present
def _configure_from_env() -> None:
- """
- Configure logging from environment variables.
+ """Configure logging from environment variables.
Reads:
- MY_PYTHON_PACKAGE_LOG_LEVEL: Logging level (debug, info, etc.)
- MY_PYTHON_PACKAGE_LOG_FILE: Path to log file
+ GREETING_TOOLKIT_LOG_LEVEL: Logging level (debug, info, etc.)
+ GREETING_TOOLKIT_LOG_FILE: Path to log file
"""
- env_level: Optional[str] = os.environ.get("MY_PYTHON_PACKAGE_LOG_LEVEL")
- env_file: Optional[str] = os.environ.get("MY_PYTHON_PACKAGE_LOG_FILE")
+ env_level: str | None = os.environ.get("GREETING_TOOLKIT_LOG_LEVEL")
+ env_file: str | None = os.environ.get("GREETING_TOOLKIT_LOG_FILE")
if env_level or env_file:
configure_logging(
@@ -154,4 +154,3 @@ def _configure_from_env() -> None:
# Default configuration
configure_logging()
_configure_from_env()
-
diff --git a/tests/__init__.py b/tests/__init__.py
index 10047b4..129a98f 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -7,29 +7,29 @@
import pytest
-import my_python_package
+import greeting_toolkit
def test_package_version():
"""Test that the package has a valid version string."""
- assert hasattr(my_python_package, "__version__")
- assert isinstance(my_python_package, str)
+ assert hasattr(greeting_toolkit, "__version__")
+ assert isinstance(greeting_toolkit, str)
# Check that it follows semantic versioning (major.minor.patch)
- assert re.match(r"^\d+\.\d+\.\d+", my_python_package.__version__)
+ assert re.match(r"^\d+\.\d+\.\d+", greeting_toolkit.__version__)
def test_package_author():
"""Test that the package has an author string."""
- assert hasattr(my_python_package, "__author__")
- assert isinstance(my_python_package.__author__, str)
- assert my_python_package.__author__ == "Diogo Ribeiro"
+ assert hasattr(greeting_toolkit, "__author__")
+ assert isinstance(greeting_toolkit.__author__, str)
+ assert greeting_toolkit.__author__ == "Diogo Ribeiro"
def test_package_exports():
"""Test that the package exports the expected functions."""
# Check __all__ contents
- assert hasattr(my_python_package, "__all__")
- assert isinstance(my_python_package.__all__, list)
+ assert hasattr(greeting_toolkit, "__all__")
+ assert isinstance(greeting_toolkit.__all__, list)
# Check expected functions are in __all__
expected_functions = [
@@ -41,24 +41,24 @@ def test_package_exports():
"format_greeting",
]
for func in expected_functions:
- assert func in my_python_package.__all__
+ assert func in greeting_toolkit.__all__
# Check functions are actually exported
- for func in my_python_package.__all__:
- assert hasattr(my_python_package, func)
- assert callable(getattr(my_python_package, func))
+ for func in greeting_toolkit.__all__:
+ assert hasattr(greeting_toolkit, func)
+ assert callable(getattr(greeting_toolkit, func))
def test_module_imports():
"""Test that all package imports work properly."""
# Test importing the main module
- importlib.reload(my_python_package)
+ importlib.reload(greeting_toolkit)
# Test importing submodules
- from my_python_package import config
- from my_python_package import core
- from my_python_package import cli
- from my_python_package import logging
+ from greeting_toolkit import config
+ from greeting_toolkit import core
+ from greeting_toolkit import cli
+ from greeting_toolkit import logging
# Verify the modules loaded correctly
assert hasattr(config, "Config")
@@ -70,10 +70,10 @@ def test_module_imports():
def test_main_function():
"""Test the _main function that handles module execution."""
# Create a mock for sys.exit and cli.main
- with patch("my_python_package.cli.main", return_value=42) as mock_main:
+ with patch("greeting_toolkit.cli.main", return_value=42) as mock_main:
with patch("sys.exit") as mock_exit:
# Call _main function
- my_python_package._main()
+ greeting_toolkit._main()
# Verify cli.main was called
mock_main.assert_called_once()
@@ -88,20 +88,20 @@ def test_direct_execution():
# but we can test the behavior indirectly by simulating it
# Save original __name__
- original_name = my_python_package.__name__
+ original_name = greeting_toolkit.__name__
try:
# Set up the module as if it's being run directly
- with patch.object(my_python_package, "__name__", "__main__"):
- with patch("my_python_package._main") as mock_main:
+ with patch.object(greeting_toolkit, "__name__", "__main__"):
+ with patch("greeting_toolkit._main") as mock_main:
# Re-execute the module code
- exec(open(my_python_package.__file__).read(), vars(my_python_package))
+ exec(open(greeting_toolkit.__file__).read(), vars(greeting_toolkit))
# Verify _main was called
mock_main.assert_called_once()
finally:
# Restore original __name__
- my_python_package.__name__ = original_name
+ greeting_toolkit.__name__ = original_name
def test_doctest_examples():
@@ -109,7 +109,7 @@ def test_doctest_examples():
import doctest
# Run doctests on the module
- result = doctest.testmod(my_python_package)
+ result = doctest.testmod(greeting_toolkit)
# Verify all tests passed (failures == 0)
assert result.failed == 0
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 27f9307..8eda790 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,12 +1,24 @@
"""Tests for the CLI module."""
-import sys
+from copy import deepcopy
from io import StringIO
from unittest.mock import patch
import pytest
-from my_python_package.cli import main, parse_args
+from greeting_toolkit.cli import main, parse_args
+from greeting_toolkit.config import DEFAULT_CONFIG
+from greeting_toolkit.config import config as global_config
+
+ORIGINAL_DEFAULTS = deepcopy(DEFAULT_CONFIG)
+
+
+@pytest.fixture(autouse=True)
+def reset_config() -> None:
+ yield
+ DEFAULT_CONFIG.clear()
+ DEFAULT_CONFIG.update(deepcopy(ORIGINAL_DEFAULTS))
+ global_config._config = deepcopy(ORIGINAL_DEFAULTS)
def test_parse_args_hello():
@@ -52,13 +64,19 @@ def test_parse_args_format():
assert not args.uppercase
assert args.max_length is None
- args = parse_args([
- "format", "World",
- "--greeting", "Hi",
- "--punctuation", ".",
- "--uppercase",
- "--max-length", "10",
- ])
+ args = parse_args(
+ [
+ "format",
+ "World",
+ "--greeting",
+ "Hi",
+ "--punctuation",
+ ".",
+ "--uppercase",
+ "--max-length",
+ "10",
+ ]
+ )
assert args.command == "format"
assert args.name == "World"
assert args.greeting == "Hi"
@@ -89,7 +107,7 @@ def test_main_no_command():
@pytest.mark.parametrize(
- "command,args,expected_output",
+ ("command", "args", "expected_output"),
[
(
"hello",
@@ -106,11 +124,11 @@ def test_main_no_command():
["World", "--uppercase"],
"HELLO, WORLD!",
),
- (
- "format",
- ["World", "--max-length", "10"],
- "Hello, ...",
- ),
+ (
+ "format",
+ ["World", "--max-length", "10"],
+ "Hello, ...",
+ ),
(
"multi",
["Alice", "Bob"],
@@ -127,7 +145,7 @@ def test_main_commands(command, args, expected_output):
@pytest.mark.parametrize(
- "command,args,expected_in_output",
+ ("command", "args", "expected_in_output"),
[
(
"random",
@@ -147,3 +165,58 @@ def test_main_variable_output_commands(command, args, expected_in_output):
result = main([command] + args)
assert result == 0
assert expected_in_output in fake_out.getvalue()
+
+
+def test_main_config_show():
+ """Test showing configuration via CLI."""
+ with patch("sys.stdout", new=StringIO()) as fake_out:
+ result = main(["config", "show"])
+ assert result == 0
+ assert "default_greeting" in fake_out.getvalue()
+
+
+def test_main_config_set():
+ """Test setting configuration values via CLI."""
+ with patch("sys.stdout", new=StringIO()) as fake_out:
+ result = main(
+ [
+ "config",
+ "set",
+ "--greeting",
+ "Hi",
+ "--punctuation",
+ ".",
+ "--title",
+ "Dr. ",
+ "--max-name-length",
+ "10",
+ ]
+ )
+ output = fake_out.getvalue()
+ assert result == 0
+ assert "Default greeting set to: Hi" in output
+ assert "Default punctuation set to: ." in output
+ assert "Formal title set to: Dr. " in output
+ assert "Max name length set to: 10" in output
+
+
+def test_main_config_add_greeting():
+ """Test adding a greeting via CLI."""
+ with patch("sys.stdout", new=StringIO()) as fake_out:
+ result = main(["config", "add-greeting", "Ahoy"])
+ output = fake_out.getvalue()
+ assert result == 0
+ assert "Added greeting: Ahoy" in output
+
+
+def test_main_config_save_and_load(tmp_path):
+ """Test saving and loading configuration via CLI."""
+ config_file = tmp_path / "config.json"
+ with patch("sys.stdout", new=StringIO()) as fake_out:
+ result = main(["config", "save", str(config_file)])
+ assert result == 0
+ assert config_file.exists()
+ with patch("sys.stdout", new=StringIO()) as fake_out:
+ result = main(["config", "load", str(config_file)])
+ assert result == 0
+ assert "Configuration loaded from" in fake_out.getvalue()
diff --git a/tests/test_config.py b/tests/test_config.py
index 4959f2e..f5548ae 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -8,7 +8,7 @@
import pytest
-from my_python_package.config import Config, DEFAULT_CONFIG
+from greeting_toolkit.config import DEFAULT_CONFIG, Config
def test_config_defaults():
@@ -123,7 +123,7 @@ def test_config_load_from_env():
try:
# Set environment variable
- with patch.dict(os.environ, {"MY_PYTHON_PACKAGE_CONFIG": tmp.name}):
+ with patch.dict(os.environ, {"GREETING_TOOLKIT_CONFIG": tmp.name}):
config = Config() # No path provided, should use env var
# Check custom values were loaded
@@ -177,7 +177,7 @@ def test_config_save():
assert config_path.exists()
# Load and verify
- with open(config_path, "r") as f:
+ with open(config_path) as f:
saved_config = json.load(f)
assert saved_config["default_greeting"] == "Hola"
@@ -201,7 +201,7 @@ def test_config_save_to_current_path():
assert config_path.exists()
# Verify content
- with open(config_path, "r") as f:
+ with open(config_path) as f:
saved_config = json.load(f)
assert saved_config["default_greeting"] == "Bonjour"
diff --git a/tests/test_core.py b/tests/test_core.py
index 7094ab7..5149335 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -6,7 +6,7 @@
import pytest
-from my_python_package.core import (
+from greeting_toolkit.core import (
create_greeting_list,
format_greeting,
generate_greeting,
@@ -56,7 +56,7 @@ def test_hello_invalid_type():
)
def test_generate_greeting_time_based(hour, expected_prefix):
"""Test time-based greetings at different hours."""
- with patch("my_python_package.core.datetime") as mock_datetime:
+ with patch("greeting_toolkit.core.datetime") as mock_datetime:
mock_datetime.now.return_value = datetime(2025, 1, 1, hour, 0, 0)
result = generate_greeting("John", time_based=True)
assert result == f"{expected_prefix}, John!"
@@ -117,7 +117,7 @@ def test_create_greeting_list_empty():
# Random greeting test
def test_random_greeting():
"""Test random greeting selection."""
- with patch("my_python_package.core.random.choice", return_value="Hi"):
+ with patch("greeting_toolkit.core.secrets.choice", return_value="Hi"):
result = random_greeting("John")
assert result == "Hi, John!"
diff --git a/tests/test_docstring_coverage_script.py b/tests/test_docstring_coverage_script.py
new file mode 100644
index 0000000..fb437a1
--- /dev/null
+++ b/tests/test_docstring_coverage_script.py
@@ -0,0 +1,14 @@
+import subprocess
+import sys
+from pathlib import Path
+
+
+def test_docstring_coverage_script_runs():
+ script = Path("scripts/check_docstrings_coverage.py")
+ result = subprocess.run(
+ [sys.executable, str(script), "--dir", "src/greeting_toolkit"],
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
diff --git a/tests/test_logging.py b/tests/test_logging.py
index 3934062..e1b4371 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -3,15 +3,13 @@
import io
import logging
import os
-import sys
-import tempfile
+import shutil
from pathlib import Path
from unittest.mock import patch
import pytest
-from my_python_package.logging import (
- DEFAULT_FORMAT,
+from greeting_toolkit.logging import (
configure_logging,
get_logger,
logger,
@@ -39,7 +37,7 @@ def reset_logger():
def test_default_logger_configuration():
"""Test the default logger configuration."""
# Verify the logger exists and has the correct name
- assert logger.name == "my_python_package"
+ assert logger.name == "greeting_toolkit"
# Verify at least one handler is configured (console output)
assert len(logger.handlers) > 0
@@ -88,9 +86,7 @@ def test_configure_logging_propagate(reset_logger):
def test_configure_logging_file(reset_logger):
"""Test configuring logging to a file."""
- # Create a temporary file for logging
- with tempfile.NamedTemporaryFile(delete=False) as tmp:
- tmp_path = tmp.name
+ tmp_path = Path("test.log")
try:
# Configure with file
@@ -102,32 +98,27 @@ def test_configure_logging_file(reset_logger):
# Verify the file handler has the correct path
for handler in file_handlers:
- assert handler.baseFilename == tmp_path
+ assert Path(handler.baseFilename) == tmp_path.resolve()
# Write a log message
test_message = "Test log message to file"
logger.info(test_message)
# Verify the message was written to the file
- with open(tmp_path, "r") as f:
- content = f.read()
- assert test_message in content
+ content = tmp_path.read_text()
+ assert test_message in content
finally:
- # Clean up
- if os.path.exists(tmp_path):
- os.unlink(tmp_path)
+ if tmp_path.exists():
+ tmp_path.unlink()
def test_configure_logging_file_directory_creation(reset_logger):
"""Test logging to a file in a non-existent directory."""
- # Create a temporary directory
- with tempfile.TemporaryDirectory() as tmp_dir:
- # Create a path to a subdirectory that doesn't exist
- log_dir = Path(tmp_dir) / "logs"
- log_file = log_dir / "test.log"
+ log_dir = Path("logs") / "subdir"
+ log_file = log_dir / "test.log"
- # Configure logging to this file
+ try:
configure_logging(log_file=log_file)
# Verify the directory was created
@@ -141,6 +132,9 @@ def test_configure_logging_file_directory_creation(reset_logger):
assert log_file.exists()
content = log_file.read_text()
assert test_message in content
+ finally:
+ if log_dir.parent.exists():
+ shutil.rmtree(log_dir.parent)
def test_get_logger():
@@ -150,7 +144,7 @@ def test_get_logger():
module_logger = get_logger(module_name)
# Verify the logger has the correct name
- assert module_logger.name == f"my_python_package.{module_name}"
+ assert module_logger.name == f"greeting_toolkit.{module_name}"
# Verify it's a proper logger instance
assert isinstance(module_logger, logging.Logger)
@@ -183,36 +177,38 @@ def test_environment_variable_configuration():
original_env = os.environ.copy()
try:
- # Create a temporary file for logging
- with tempfile.NamedTemporaryFile(delete=False) as tmp:
- tmp_path = tmp.name
+ tmp_path = Path("env.log")
- # Directly call the function we want to test
- from my_python_package.logging import _configure_from_env
+ from greeting_toolkit.logging import _configure_from_env
- # Set environment variables
- os.environ["MY_PYTHON_PACKAGE_LOG_LEVEL"] = "DEBUG"
- os.environ["MY_PYTHON_PACKAGE_LOG_FILE"] = tmp_path
+ os.environ["GREETING_TOOLKIT_LOG_LEVEL"] = "DEBUG"
+ os.environ["GREETING_TOOLKIT_LOG_FILE"] = str(tmp_path)
- # Call the function with patching
- with patch("my_python_package.logging.configure_logging") as mock_configure:
+ with patch("greeting_toolkit.logging.configure_logging") as mock_configure:
_configure_from_env()
- # Verify the function was called with the right parameters
mock_configure.assert_called_once()
args, kwargs = mock_configure.call_args
assert kwargs.get("level") == "DEBUG"
- assert kwargs.get("log_file") == tmp_path
+ assert Path(kwargs.get("log_file")) == tmp_path
finally:
- # Clean up
- if os.path.exists(tmp_path):
- os.unlink(tmp_path)
- # Restore original environment
+ if tmp_path.exists():
+ tmp_path.unlink()
os.environ.clear()
os.environ.update(original_env)
+def test_configure_logging_rejects_external_path(reset_logger):
+ """Logging to paths outside CWD should be ignored."""
+ outside = Path("/etc/passwd")
+ with patch.object(logger, "warning") as warn:
+ configure_logging(log_file=outside)
+ warn.assert_called()
+
+ assert not any(isinstance(h, logging.FileHandler) for h in logger.handlers)
+
+
def test_logging_levels(reset_logger):
"""Test different logging levels."""
# Create a StringIO for capturing output
@@ -277,4 +273,4 @@ def test_nested_logger():
nested_logger = get_logger("module.submodule")
# Verify the logger has the correct name
- assert nested_logger.name == "my_python_package.module.submodule"
+ assert nested_logger.name == "greeting_toolkit.module.submodule"
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..d28f266
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,16 @@
+"""Tests for invoking the package as a module."""
+
+import subprocess
+import sys
+
+
+def test_module_execution():
+ """Running ``python -m greeting_toolkit`` should execute the CLI."""
+ result = subprocess.run( # noqa: S603
+ [sys.executable, "-m", "greeting_toolkit", "hello", "World"],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ assert result.returncode == 0
+ assert "Hello, World!" in result.stdout
diff --git a/tests/test_package.py b/tests/test_package.py
new file mode 100644
index 0000000..948c6a3
--- /dev/null
+++ b/tests/test_package.py
@@ -0,0 +1,108 @@
+"""Tests for the package __init__ module."""
+
+import importlib
+import re
+from unittest.mock import patch
+
+import pytest
+
+import greeting_toolkit
+
+
+def test_package_version():
+ """Test that the package has a valid version string."""
+ assert hasattr(greeting_toolkit, "__version__")
+ assert isinstance(greeting_toolkit.__version__, str)
+ # Check that it follows semantic versioning (major.minor.patch)
+ assert re.match(r"^\d+\.\d+\.\d+", greeting_toolkit.__version__)
+
+
+def test_package_author():
+ """Test that the package has an author string."""
+ assert hasattr(greeting_toolkit, "__author__")
+ assert isinstance(greeting_toolkit.__author__, str)
+ assert greeting_toolkit.__author__ == "Diogo Ribeiro"
+
+
+def test_package_exports():
+ """Test that the package exports the expected functions."""
+ # Check __all__ contents
+ assert hasattr(greeting_toolkit, "__all__")
+ assert isinstance(greeting_toolkit.__all__, list)
+
+ # Check expected functions are in __all__
+ expected_functions = [
+ "hello",
+ "generate_greeting",
+ "random_greeting",
+ "validate_name",
+ "create_greeting_list",
+ "format_greeting",
+ ]
+ for func in expected_functions:
+ assert func in greeting_toolkit.__all__
+
+ # Check functions are actually exported
+ for func in greeting_toolkit.__all__:
+ assert hasattr(greeting_toolkit, func)
+ assert callable(getattr(greeting_toolkit, func))
+
+
+def test_module_imports():
+ """Test that all package imports work properly."""
+ # Test importing the main module
+ importlib.reload(greeting_toolkit)
+
+ # Test importing submodules
+ from greeting_toolkit import cli, config, core, logging
+
+ # Verify the modules loaded correctly
+ assert hasattr(config, "Config")
+ assert hasattr(core, "hello")
+ assert hasattr(cli, "main")
+ assert hasattr(logging, "logger")
+
+
+def test_main_function():
+ """Test the _main function that handles module execution."""
+ # Create a mock for sys.exit and cli.main
+ with patch("greeting_toolkit.cli.main", return_value=42) as mock_main:
+ with patch("sys.exit") as mock_exit:
+ # Call _main function
+ greeting_toolkit._main()
+
+ # Verify cli.main was called
+ mock_main.assert_called_once()
+
+ # Verify sys.exit was called with the return value from cli.main
+ mock_exit.assert_called_once_with(42)
+
+
+def test_direct_execution():
+ """Test module execution behavior."""
+ # We can't directly test __name__ == "__main__" code paths in an imported module,
+ # but we can test the behavior indirectly by simulating it
+
+ # Save original __name__
+ original_name = greeting_toolkit.__name__
+
+ try:
+ # Set up the module as if it's being run directly
+ with patch.object(greeting_toolkit, "__name__", "__main__"):
+ with pytest.raises(SystemExit):
+ exec(open(greeting_toolkit.__file__).read(), vars(greeting_toolkit))
+ finally:
+ # Restore original __name__
+ greeting_toolkit.__name__ = original_name
+
+
+def test_doctest_examples():
+ """Test that the doctest examples in __init__ work correctly."""
+ import doctest
+
+ # Run doctests on the module
+ result = doctest.testmod(greeting_toolkit)
+
+ # Verify all tests passed (failures == 0)
+ assert result.failed == 0
+ assert result.attempted > 0 # Make sure some tests were actually run
diff --git a/tox.ini b/tox.ini
index f15d622..24697c0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ deps =
pytest-mock
pytest-sugar
commands =
- pytest {posargs:tests} --cov=my_python_package --cov-report=term-missing
+ pytest {posargs:tests} --cov=greeting_toolkit --cov-report=term-missing
[testenv:lint]
deps =
@@ -40,4 +40,4 @@ commands =
deps =
pdoc
commands =
- pdoc --html --output-dir docs src/my_python_package
+ pdoc --html --output-dir docs src/greeting_toolkit