Skip to content

Commit 8d6b48e

Browse files
committed
feat: Finalize comprehensive fixes for issues #2-#11 and Copilot review
Implemented robust error handling in encryptpdf.py. Updated CLI with type hints and return codes. comprehensive unit and integration tests. Addressed all Copilot feedback regarding imports and docs.
1 parent 81135f7 commit 8d6b48e

6 files changed

Lines changed: 130 additions & 44 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ jobs:
2525
python -m pip install --upgrade pip
2626
pip install poetry
2727
poetry install
28-
- name: Debug Environment
29-
run: |
30-
poetry env info
31-
poetry show
32-
poetry run pip list
3328
- name: Test with unittest
3429
run: |
3530
poetry run coverage run -m unittest discover tests

passifypdf/cli.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import argparse
22

33

4-
def get_arg_parser():
5-
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"))
4+
def get_arg_parser() -> argparse.ArgumentParser:
5+
arg_parser = argparse.ArgumentParser(
6+
description="Encrypt a PDF file with a password of your choice.",
7+
epilog="For more information, visit: https://github.com/SUPAIDEAS/passifypdf"
8+
)
69
arg_parser.add_argument("-v", "--version", action="version", version="%(prog)s 1.0")
7-
arg_parser.add_argument("-i", "--input", required=True, help="path to the input pdf file")
8-
arg_parser.add_argument("-o", "--output", required=True, help="path to the output encrypted pdf file")
9-
arg_parser.add_argument("-p", "--passwd", required=True, type=str, help="password to encrypt the pdf file")
10+
arg_parser.add_argument("-i", "--input", required=True, help="Path to the input PDF file to be encrypted")
11+
arg_parser.add_argument("-o", "--output", required=True, help="Path where the encrypted PDF file will be saved")
12+
arg_parser.add_argument("-p", "--passwd", required=True, type=str, help="Password to encrypt the PDF file with")
1013
return arg_parser

passifypdf/encryptpdf.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,68 @@
22
from pypdf import PdfReader, PdfWriter
33

44

5-
def encrypt_pdf(input_pdf, output_pdf, password):
6-
reader = PdfReader(input_pdf)
7-
writer = PdfWriter()
5+
import sys
6+
import os
7+
from pathlib import Path
8+
from typing import Union
89

9-
# Add all pages from the reader to the writer
10-
for page_num in range(len(reader.pages)):
11-
writer.add_page(reader.pages[page_num])
10+
def encrypt_pdf(input_pdf: Union[str, Path], output_pdf: Union[str, Path], password: str) -> None:
11+
"""
12+
Encrypts a PDF file with a password.
1213
13-
# Encrypt with a password
14-
writer.encrypt(password)
14+
Args:
15+
input_pdf (Union[str, Path]): Path to the input PDF file.
16+
output_pdf (Union[str, Path]): Path to the output PDF file.
17+
password (str): Password to encrypt the PDF with.
1518
16-
# Write the encrypted PDF to a new PDF file passed as param
17-
with open(output_pdf, "wb") as f:
18-
writer.write(f)
19+
Raises:
20+
FileNotFoundError: If the input PDF file does not exist.
21+
Exception: For other errors during processing.
22+
"""
23+
input_path = Path(input_pdf)
24+
if not input_path.exists():
25+
raise FileNotFoundError(f"Input file '{input_pdf}' not found.")
26+
27+
if not input_path.is_file():
28+
raise IsADirectoryError(f"Input path '{input_pdf}' is not a file.")
1929

30+
try:
31+
reader = PdfReader(input_path)
32+
writer = PdfWriter()
2033

21-
def pipeline(any_thing):
22-
return any_thing
34+
# Add all pages from the reader to the writer
35+
for page in reader.pages:
36+
writer.add_page(page)
2337

38+
# Encrypt with a password
39+
writer.encrypt(password)
2440

25-
def main():
41+
# Write the encrypted PDF to a new PDF file passed as param
42+
with open(output_pdf, "wb") as f:
43+
writer.write(f)
44+
45+
except Exception as e:
46+
raise Exception(f"Failed to encrypt PDF: {e}")
47+
48+
49+
def main() -> int:
50+
"""
51+
Main function to run the CLI.
52+
53+
Returns:
54+
int: Exit code (0 for success, 1 for failure).
55+
"""
2656
arg_parser = get_arg_parser()
2757
args = arg_parser.parse_args()
28-
encrypt_pdf(args.input, args.output, args.passwd)
29-
print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'")
58+
59+
try:
60+
encrypt_pdf(args.input, args.output, args.passwd)
61+
print(f"Congratulations!\nPDF file encrypted successfully and saved as '{args.output}'")
62+
return 0
63+
except Exception as e:
64+
print(f"Error: {e}", file=sys.stderr)
65+
return 1
3066

3167

3268
if __name__ == "__main__":
33-
main()
69+
sys.exit(main())
Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
11
from unittest import TestCase
22

3-
from passifypdf.encryptpdf import pipeline
43

54

5+
6+
import os
7+
from pathlib import Path
8+
from pypdf import PdfWriter, PdfReader
9+
from passifypdf.encryptpdf import encrypt_pdf
10+
611
class TestPdfIntegrationTests(TestCase):
7-
def test_pipeline_integration(self):
8-
self.assertEqual("awesomePdfProtection-Integration", pipeline("awesomePdfProtection-Integration"))
12+
def setUp(self):
13+
self.input_pdf = "test_input.pdf"
14+
self.output_pdf = "test_output.pdf"
15+
self.password = "strongpassword"
16+
17+
# Create a dummy PDF
18+
writer = PdfWriter()
19+
writer.add_blank_page(width=100, height=100)
20+
with open(self.input_pdf, "wb") as f:
21+
writer.write(f)
22+
23+
def tearDown(self):
24+
# Cleanup files
25+
if os.path.exists(self.input_pdf):
26+
os.remove(self.input_pdf)
27+
if os.path.exists(self.output_pdf):
28+
os.remove(self.output_pdf)
29+
30+
def test_encrypt_pdf_integration(self):
31+
# Encrypt the PDF
32+
encrypt_pdf(self.input_pdf, self.output_pdf, self.password)
33+
34+
# Verify output exists
35+
self.assertTrue(os.path.exists(self.output_pdf))
36+
37+
# Verify it is encrypted
38+
reader = PdfReader(self.output_pdf)
39+
self.assertTrue(reader.is_encrypted)
40+
41+
# Verify we can decrypt it
42+
self.assertTrue(reader.decrypt(self.password))
43+
self.assertEqual(len(reader.pages), 1)

tests/test_encryptpdf_example.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/unittests/test_encryptpdf.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from unittest import TestCase
22
from unittest.mock import patch, mock_open
3-
from passifypdf.encryptpdf import pipeline, encrypt_pdf
3+
from passifypdf.encryptpdf import encrypt_pdf
44

55

66
class TestPdfUnitTests(TestCase):
7-
def test_pipeline(self):
8-
self.assertEqual("awesomePdfProtection-UnitTest", pipeline("awesomePdfProtection-UnitTest"))
97

108
@patch('passifypdf.encryptpdf.PdfReader')
119
@patch('passifypdf.encryptpdf.PdfWriter')
10+
@patch('passifypdf.encryptpdf.Path') # Mock Path
1211
@patch('builtins.open', new_callable=mock_open)
13-
def test_encrypt_pdf(self, mock_file, mock_writer_cls, mock_reader_cls):
12+
def test_encrypt_pdf(self, mock_file, mock_path_cls, mock_writer_cls, mock_reader_cls):
1413
# Setup mocks
14+
mock_path_instance = mock_path_cls.return_value
15+
mock_path_instance.exists.return_value = True
16+
mock_path_instance.is_file.return_value = True
17+
1518
mock_reader_instance = mock_reader_cls.return_value
1619
mock_reader_instance.pages = ['page1', 'page2']
1720

@@ -20,8 +23,13 @@ def test_encrypt_pdf(self, mock_file, mock_writer_cls, mock_reader_cls):
2023
# Call the function
2124
encrypt_pdf("input.pdf", "output.pdf", "secret")
2225

23-
# Verify PdfReader was called
24-
mock_reader_cls.assert_called_with("input.pdf")
26+
# Verify Path existence check
27+
mock_path_cls.assert_called_with("input.pdf")
28+
mock_path_instance.exists.assert_called()
29+
mock_path_instance.is_file.assert_called()
30+
31+
# Verify PdfReader was called with the path object
32+
mock_reader_cls.assert_called_with(mock_path_instance)
2533

2634
# Verify pages were added
2735
self.assertEqual(mock_writer_instance.add_page.call_count, 2)
@@ -34,3 +42,20 @@ def test_encrypt_pdf(self, mock_file, mock_writer_cls, mock_reader_cls):
3442
# Verify file write
3543
mock_file.assert_called_with("output.pdf", "wb")
3644
mock_writer_instance.write.assert_called_with(mock_file())
45+
46+
@patch('passifypdf.encryptpdf.Path')
47+
def test_encrypt_pdf_file_not_found(self, mock_path_cls):
48+
mock_path_instance = mock_path_cls.return_value
49+
mock_path_instance.exists.return_value = False
50+
51+
with self.assertRaises(FileNotFoundError):
52+
encrypt_pdf("nonexistent.pdf", "output.pdf", "secret")
53+
54+
@patch('passifypdf.encryptpdf.Path')
55+
def test_encrypt_pdf_is_directory(self, mock_path_cls):
56+
mock_path_instance = mock_path_cls.return_value
57+
mock_path_instance.exists.return_value = True
58+
mock_path_instance.is_file.return_value = False
59+
60+
with self.assertRaises(IsADirectoryError):
61+
encrypt_pdf("directory", "output.pdf", "secret")

0 commit comments

Comments
 (0)