Skip to content

feat: add type hints, logging, pre-commit, exit code docs, and Python API docs#48

Open
ambicuity wants to merge 5 commits intoSUPAIDEAS:mainfrom
ambicuity:fix/cli-enhancements-and-docs
Open

feat: add type hints, logging, pre-commit, exit code docs, and Python API docs#48
ambicuity wants to merge 5 commits intoSUPAIDEAS:mainfrom
ambicuity:fix/cli-enhancements-and-docs

Conversation

@ambicuity
Copy link
Copy Markdown
Contributor

@ambicuity ambicuity commented Feb 20, 2026

Summary

This PR addresses the following open issues:

Testing

All 15 existing tests pass with 89% code coverage:

15 passed in 1.20s
passifypdf/__init__.py    0      0   100%
passifypdf/cli.py        14      0   100%
passifypdf/encryptpdf.py 41      6    85%
TOTAL                    55      6    89%

ambicuity and others added 3 commits October 23, 2025 01:44
Add unit tests for encrypt_pdf function to verify functionality, including output file creation, encryption status, decryption with correct password, page count preservation, and handling of invalid input.
Copilot AI review requested due to automatic review settings February 20, 2026 15:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds three key features to address reported issues: a dynamic version flag that reads from package metadata, an interactive overwrite prompt with a --force bypass option, and comprehensive contributor documentation.

Changes:

  • Added dynamic --version/-v flag using importlib.metadata to retrieve version from installed package
  • Implemented file overwrite confirmation prompt with --force flag to bypass
  • Created CONTRIBUTING.md with setup instructions, testing guidelines, and PR submission process

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
passifypdf/cli.py Added dynamic version retrieval and --force flag argument
passifypdf/encryptpdf.py Added output file existence check with interactive overwrite prompt
tests/unittests/test_encryptpdf.py Added comprehensive tests for encryption function and overwrite prompt behavior; renamed test class for clarity
CONTRIBUTING.md Added contributor guide with uv setup, testing, and PR submission instructions
poetry.lock Generated lock file with dependency specifications

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/unittests/test_encryptpdf.py Outdated
Comment on lines +23 to +28
def tearDown(self):
"""Clean up test files."""
if os.path.exists(self.output_pdf):
os.remove(self.output_pdf)
if os.path.exists(self.temp_dir):
os.rmdir(self.temp_dir)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test tearDown may fail if the temp_dir is not empty. The code removes the output_pdf file but then tries to remove the temp_dir with os.rmdir(), which only works for empty directories. If there are any other files created in temp_dir during test execution, this will raise an OSError. Consider using shutil.rmtree(self.temp_dir) instead, or check if the directory is empty before attempting removal.

Copilot uses AI. Check for mistakes.
Comment thread tests/unittests/test_encryptpdf.py Outdated

self.assertEqual(result, 0)
mock_encrypt_pdf.assert_not_called()
mock_print.assert_called_with("Operation cancelled.")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the case when the output file does NOT exist. The overwrite prompt should not be shown when the output file doesn't exist, and encryption should proceed directly. Consider adding a test case like "test_main_without_force_flag_file_not_exists" to verify this behavior is correct.

Suggested change
mock_print.assert_called_with("Operation cancelled.")
mock_print.assert_called_with("Operation cancelled.")
@patch('passifypdf.encryptpdf.get_arg_parser')
@patch('passifypdf.encryptpdf.encrypt_pdf')
@patch('passifypdf.encryptpdf.Path')
@patch('builtins.print')
@patch('builtins.input')
def test_main_without_force_flag_file_not_exists(self, mock_input, mock_print, mock_path_cls, mock_encrypt_pdf, mock_arg_parser):
"""Test main() without --force flag when output file does not exist."""
mock_parser_instance = mock_arg_parser.return_value
mock_args = type('Args', (), {'input': 'in.pdf', 'output': 'out.pdf', 'passwd': 'pass', 'force': False})()
mock_parser_instance.parse_args.return_value = mock_args
mock_path_instance = mock_path_cls.return_value
mock_path_instance.exists.return_value = False
from passifypdf.encryptpdf import main
result = main()
self.assertEqual(result, 0)
mock_encrypt_pdf.assert_called_with('in.pdf', 'out.pdf', 'pass')
mock_input.assert_not_called()

Copilot uses AI. Check for mistakes.
Comment thread passifypdf/encryptpdf.py Outdated

try:
output_path = Path(args.output)
if output_path.exists() and not getattr(args, 'force', False):
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of getattr(args, 'force', False) is unnecessarily defensive since the --force argument is now explicitly defined in the argument parser with action="store_true", which guarantees the 'force' attribute will always exist on args (defaulting to False). Using args.force directly would be clearer and more consistent with how other arguments like args.input, args.output, and args.passwd are accessed in the same function.

Suggested change
if output_path.exists() and not getattr(args, 'force', False):
if output_path.exists() and not args.force:

Copilot uses AI. Check for mistakes.
Comment thread passifypdf/cli.py
Comment on lines +6 to +15
try:
__version__ = version("passifypdf")
except PackageNotFoundError:
__version__ = "unknown"

arg_parser = argparse.ArgumentParser(
description="Encrypt a PDF file with a password of your choice.",
epilog="For more information, visit: https://github.com/SUPAIDEAS/passifypdf"
)
arg_parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0")
arg_parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new --version/-v flag functionality. The dynamic version retrieval using importlib.metadata should be tested to ensure it works correctly both when the package is installed and when it's not (PackageNotFoundError case). Consider adding tests that verify the version flag returns the correct version string.

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +162
@patch('passifypdf.encryptpdf.get_arg_parser')
@patch('passifypdf.encryptpdf.encrypt_pdf')
@patch('passifypdf.encryptpdf.Path')
@patch('builtins.print')
def test_main_with_force_flag(self, mock_print, mock_path_cls, mock_encrypt_pdf, mock_arg_parser):
"""Test main() with --force flag when output file exists."""
mock_parser_instance = mock_arg_parser.return_value
mock_args = type('Args', (), {'input': 'in.pdf', 'output': 'out.pdf', 'passwd': 'pass', 'force': True})()
mock_parser_instance.parse_args.return_value = mock_args

mock_path_instance = mock_path_cls.return_value
mock_path_instance.exists.return_value = True

from passifypdf.encryptpdf import main
result = main()

self.assertEqual(result, 0)
mock_encrypt_pdf.assert_called_with('in.pdf', 'out.pdf', 'pass')
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the test mocks, Path is mocked at the class level, but the tests don't verify that Path was called with the correct argument (args.output). Consider adding an assertion like mock_path_cls.assert_called_with('out.pdf') to ensure the output path is being checked correctly, similar to how test_encrypt_pdf verifies Path is called with the input path.

Copilot uses AI. Check for mistakes.
…EAS#41, SUPAIDEAS#42

- feat(tests): add -> None return type hints to all test methods
  and fixtures in test_encryptpdf.py and test_cli.py (closes SUPAIDEAS#42)
- feat(ci): add .pre-commit-config.yaml with ruff and pre-commit-hooks;
  update CONTRIBUTING.md with pre-commit setup instructions (closes SUPAIDEAS#41)
- refactor: replace print() calls with standard logging module in
  encryptpdf.py; update test mocks to patch logger instead of
  builtins.print (closes SUPAIDEAS#40)
- docs: add Exit Codes section and --force flag to docs/CLI_OPTIONS.md;
  fix outdated poetry references to use uv (closes SUPAIDEAS#37)
- docs: add Programmatic Usage (Python API) section to README.md;
  update install/usage instructions from poetry to uv;
  fix typos ('nay' -> 'any', 'complain' -> 'complaint', etc.) (closes SUPAIDEAS#36)
@ambicuity ambicuity changed the title feat: Add dynamic version flag, handle output collisions, add CONTRIBUTING.md feat: add type hints, logging, pre-commit, exit code docs, and Python API docs Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants