Skip to content

Commit 3ca91c6

Browse files
committed
fix: Refactor once more with overload
1 parent 0db06c8 commit 3ca91c6

File tree

2 files changed

+122
-104
lines changed

2 files changed

+122
-104
lines changed

src/c2pa/c2pa.py

Lines changed: 42 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import warnings
77
from pathlib import Path
8-
from typing import Optional, Union, Callable, Any
8+
from typing import Optional, Union, Callable, Any, overload
99
import time
1010
from .lib import dynamically_load_library
1111
import mimetypes
@@ -578,7 +578,7 @@ def read_file(path: Union[str, Path],
578578
C2paError: If there was an error reading the file
579579
"""
580580
warnings.warn(
581-
"The read_file function is deprecated and will be removed in a future version. "
581+
"The read_file function is deprecated and will be removed in a future version."
582582
"Please use the Reader class for reading C2PA metadata instead.",
583583
DeprecationWarning,
584584
stacklevel=2)
@@ -592,26 +592,50 @@ def read_file(path: Union[str, Path],
592592
return _parse_operation_result_for_error(result)
593593

594594

595+
@overload
595596
def sign_file(
596597
source_path: Union[str, Path],
597598
dest_path: Union[str, Path],
598599
manifest: str,
599-
signer_info: C2paSignerInfo,
600-
data_dir: Optional[Union[str, Path]] = None
600+
signer_info: C2paSignerInfo
601601
) -> str:
602-
"""Sign a file with a C2PA manifest.
603-
For now, this function is left here to provide a backwards-compatible API.
602+
"""Sign a file with a C2PA manifest using signer info.
603+
604+
.. deprecated:: 0.10.0
605+
This function is deprecated and will be removed in a future version.
606+
Please use the Builder class for signing and the Reader class for reading signed data instead.
607+
"""
608+
...
609+
610+
@overload
611+
def sign_file(
612+
source_path: Union[str, Path],
613+
dest_path: Union[str, Path],
614+
manifest: str,
615+
signer: 'Signer'
616+
) -> str:
617+
"""Sign a file with a C2PA manifest using a signer.
604618
605619
.. deprecated:: 0.10.0
606620
This function is deprecated and will be removed in a future version.
607621
Please use the Builder class for signing and the Reader class for reading signed data instead.
622+
"""
623+
...
624+
625+
def sign_file(
626+
source_path: Union[str, Path],
627+
dest_path: Union[str, Path],
628+
manifest: str,
629+
signer_or_info: Union[C2paSignerInfo, 'Signer']
630+
) -> str:
631+
"""Sign a file with a C2PA manifest.
632+
For now, this function is left here to provide a backwards-compatible API.
608633
609634
Args:
610635
source_path: Path to the source file
611636
dest_path: Path to write the signed file to
612637
manifest: The manifest JSON string
613-
signer_info: Signing configuration
614-
data_dir: Optional directory to write binary resources to
638+
signer_or_info: Either a signer configuration or a signer object
615639
616640
Returns:
617641
The signed manifest as a JSON string
@@ -621,15 +645,15 @@ def sign_file(
621645
C2paError.Encoding: If any of the string inputs contain invalid UTF-8 characters
622646
C2paError.NotSupported: If the file type cannot be determined
623647
"""
624-
warnings.warn(
625-
"The sign_file function is deprecated and will be removed in a future version. "
626-
"Please use the Builder class for signing and the Reader class for reading signed data instead.",
627-
DeprecationWarning,
628-
stacklevel=2)
629648

630649
try:
631-
# Create a signer from the signer info
632-
signer = Signer.from_info(signer_info)
650+
# Determine if we have a signer or signer info
651+
if isinstance(signer_or_info, C2paSignerInfo):
652+
signer = Signer.from_info(signer_or_info)
653+
own_signer = True
654+
else:
655+
signer = signer_or_info
656+
own_signer = False
633657

634658
# Create a builder from the manifest
635659
builder = Builder(manifest)
@@ -643,19 +667,13 @@ def sign_file(
643667
f"Could not determine MIME type for file: {source_path}")
644668

645669
# Sign the file using the builder
646-
manifest_bytes = builder.sign(
670+
builder.sign(
647671
signer=signer,
648672
format=mime_type,
649673
source=source_file,
650674
dest=dest_file
651675
)
652676

653-
# If we have manifest bytes and a data directory, write them
654-
if manifest_bytes and data_dir:
655-
manifest_path = os.path.join(str(data_dir), 'manifest.json')
656-
with open(manifest_path, 'wb') as f:
657-
f.write(manifest_bytes)
658-
659677
# Read the signed manifest from the destination file
660678
with Reader(dest_path) as reader:
661679
return reader.json()
@@ -674,73 +692,10 @@ def sign_file(
674692
# Ensure resources are cleaned up
675693
if 'builder' in locals():
676694
builder.close()
677-
if 'signer' in locals():
695+
if 'signer' in locals() and own_signer:
678696
signer.close()
679697

680698

681-
def sign_file_using_callback_signer(
682-
source_path: Union[str, Path],
683-
dest_path: Union[str, Path],
684-
manifest: str,
685-
signer: 'Signer'
686-
) -> bytes:
687-
"""Sign a file with a C2PA manifest using a signer.
688-
689-
This function provides a shortcut to sign files using a signer
690-
and returns the raw manifest bytes.
691-
692-
Args:
693-
source_path: Path to the source file
694-
dest_path: Path to write the signed file to
695-
manifest: The manifest JSON string
696-
signer: The signer to use
697-
698-
Returns:
699-
The manifest bytes (binary data)
700-
701-
Raises:
702-
C2paError: If there was an error signing the file
703-
C2paError.Encoding: If any of the string inputs contain invalid UTF-8 characters
704-
C2paError.NotSupported: If the file type cannot be determined
705-
"""
706-
try:
707-
# Create a builder from the manifest
708-
builder = Builder(manifest)
709-
710-
# Open source and destination files
711-
with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file:
712-
# Get the MIME type from the file extension
713-
mime_type = mimetypes.guess_type(str(source_path))[0]
714-
if not mime_type:
715-
raise C2paError.NotSupported(
716-
f"Could not determine MIME type for file: {source_path}")
717-
718-
# Convert Python streams to Stream objects
719-
source_stream = Stream(source_file)
720-
dest_stream = Stream(dest_file)
721-
722-
# Use the builder's internal signing logic
723-
result, manifest_bytes = builder._sign_internal(
724-
signer, mime_type, source_stream, dest_stream)
725-
726-
return manifest_bytes
727-
728-
except Exception as e:
729-
# Clean up destination file if it exists and there was an error
730-
if os.path.exists(dest_path):
731-
try:
732-
os.remove(dest_path)
733-
except OSError:
734-
pass # Ignore cleanup errors
735-
736-
# Re-raise the error
737-
raise C2paError(f"Error signing file: {str(e)}")
738-
finally:
739-
# Ensure resources are cleaned up
740-
if 'builder' in locals():
741-
builder.close()
742-
743-
744699
class Stream:
745700
# Class-level counter for generating unique stream IDs
746701
# (useful for tracing streams usage in debug)
@@ -2093,12 +2048,11 @@ def ed25519_sign(data: bytes, private_key: str) -> bytes:
20932048
'Reader',
20942049
'Builder',
20952050
'Signer',
2096-
'version',
20972051
'load_settings',
20982052
'read_file',
20992053
'read_ingredient_file',
21002054
'sign_file',
2101-
'sign_file_using_callback_signer',
21022055
'format_embeddable',
2056+
'version',
21032057
'sdk_version'
21042058
]

tests/test_unit_tests.py

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import shutil
2626

2727
from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version
28-
from c2pa.c2pa import Stream, read_ingredient_file, read_file, sign_file, load_settings, create_signer, sign_file_using_callback_signer
28+
from c2pa.c2pa import Stream, read_ingredient_file, read_file, sign_file, load_settings, create_signer
2929

3030
# Suppress deprecation warnings
3131
warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -896,7 +896,7 @@ def sign_callback(data: bytes) -> bytes:
896896
shutil.rmtree(temp_dir)
897897

898898
def test_sign_file_using_callback_signer(self):
899-
"""Test signing a file using the sign_file_using_callback_signer function."""
899+
"""Test signing a file using the sign_file function with a Signer object."""
900900
# Create a temporary directory for the test
901901
temp_dir = tempfile.mkdtemp()
902902
try:
@@ -936,23 +936,25 @@ def sign_callback(data: bytes) -> bytes:
936936
tsa_url="http://timestamp.digicert.com"
937937
)
938938

939-
# Import the new function
940-
from c2pa.c2pa import sign_file_using_callback_signer
941-
942-
# Use the sign_file_using_callback_signer function
943-
manifest_bytes = sign_file_using_callback_signer(
944-
source_path=self.testPath,
945-
dest_path=output_path,
946-
manifest=self.manifestDefinition,
947-
signer=signer
939+
# Use the overloaded sign_file function with a Signer object
940+
result_json = sign_file(
941+
self.testPath,
942+
output_path,
943+
self.manifestDefinition,
944+
signer
948945
)
949946

950947
# Verify the output file was created
951948
self.assertTrue(os.path.exists(output_path))
952949

953-
# Verify the manifest bytes are binary data (not JSON text)
954-
self.assertIsInstance(manifest_bytes, bytes)
955-
self.assertGreater(len(manifest_bytes), 0)
950+
# Verify the result is a JSON string (not binary data)
951+
self.assertIsInstance(result_json, str)
952+
self.assertGreater(len(result_json), 0)
953+
954+
# Parse the JSON and verify it contains expected content
955+
manifest_data = json.loads(result_json)
956+
self.assertIn("manifests", manifest_data)
957+
self.assertIn("active_manifest", manifest_data)
956958

957959
# Read the signed file and verify the manifest contains expected content
958960
with open(output_path, "rb") as file:
@@ -965,6 +967,69 @@ def sign_callback(data: bytes) -> bytes:
965967
# Clean up the temporary directory
966968
shutil.rmtree(temp_dir)
967969

970+
def test_sign_file_overloads(self):
971+
"""Test that the overloaded sign_file function works with both parameter types."""
972+
# Create a temporary directory for the test
973+
temp_dir = tempfile.mkdtemp()
974+
try:
975+
# Test with C2paSignerInfo
976+
output_path_1 = os.path.join(temp_dir, "signed_output_1.jpg")
977+
978+
# Load test certificates and key
979+
with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
980+
certs = cert_file.read()
981+
with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
982+
key = key_file.read()
983+
984+
# Create signer info
985+
signer_info = C2paSignerInfo(
986+
alg=b"es256",
987+
sign_cert=certs,
988+
private_key=key,
989+
ta_url=b"http://timestamp.digicert.com"
990+
)
991+
992+
# Test with C2paSignerInfo parameter
993+
result_1 = sign_file(
994+
self.testPath,
995+
output_path_1,
996+
self.manifestDefinition,
997+
signer_info
998+
)
999+
1000+
self.assertIsInstance(result_1, str)
1001+
self.assertTrue(os.path.exists(output_path_1))
1002+
1003+
# Test with Signer object
1004+
output_path_2 = os.path.join(temp_dir, "signed_output_2.jpg")
1005+
1006+
# Create a signer from the signer info
1007+
signer = Signer.from_info(signer_info)
1008+
1009+
# Test with Signer parameter
1010+
result_2 = sign_file(
1011+
self.testPath,
1012+
output_path_2,
1013+
self.manifestDefinition,
1014+
signer
1015+
)
1016+
1017+
self.assertIsInstance(result_2, str)
1018+
self.assertTrue(os.path.exists(output_path_2))
1019+
1020+
# Both results should be similar (same manifest structure)
1021+
manifest_1 = json.loads(result_1)
1022+
manifest_2 = json.loads(result_2)
1023+
1024+
self.assertIn("manifests", manifest_1)
1025+
self.assertIn("manifests", manifest_2)
1026+
self.assertIn("active_manifest", manifest_1)
1027+
self.assertIn("active_manifest", manifest_2)
1028+
1029+
finally:
1030+
# Clean up the temporary directory
1031+
shutil.rmtree(temp_dir)
1032+
9681033
class TestStream(unittest.TestCase):
9691034
def setUp(self):
9701035
# Create a temporary file for testing
@@ -1212,8 +1277,7 @@ def test_sign_file(self):
12121277
self.testPath,
12131278
output_path,
12141279
manifest_json,
1215-
signer_info,
1216-
temp_data_dir
1280+
signer_info
12171281
)
12181282

12191283
finally:

0 commit comments

Comments
 (0)