python-pdffiller 1.1.2__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/PKG-INFO +2 -2
  2. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/_version.py +1 -1
  3. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/dump_data_fields.py +2 -1
  4. python_pdffiller-2.0.0/pdffiller/pdf.py +221 -0
  5. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/typing.py +1 -2
  6. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pyproject.toml +3 -2
  7. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/PKG-INFO +2 -2
  8. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/SOURCES.txt +3 -1
  9. python_pdffiller-2.0.0/python_pdffiller.egg-info/requires.txt +3 -0
  10. python_pdffiller-2.0.0/requirements.txt +3 -0
  11. python_pdffiller-2.0.0/tests/cli/test_dump_data_field.py +106 -0
  12. python_pdffiller-2.0.0/tests/cli/test_fill_form.py +103 -0
  13. python_pdffiller-2.0.0/tests/conftest.py +70 -0
  14. python_pdffiller-2.0.0/tests/unit/__init__.py +0 -0
  15. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_form_field.py +6 -5
  16. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tox.ini +17 -3
  17. python_pdffiller-1.1.2/pdffiller/io/custom_pdf_writer.py +0 -143
  18. python_pdffiller-1.1.2/pdffiller/pdf.py +0 -351
  19. python_pdffiller-1.1.2/python_pdffiller.egg-info/requires.txt +0 -3
  20. python_pdffiller-1.1.2/requirements.txt +0 -3
  21. python_pdffiller-1.1.2/tests/conftest.py +0 -28
  22. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/AUTHORS.rst +0 -0
  23. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/CHANGELOG.md +0 -0
  24. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/COPYING +0 -0
  25. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/MANIFEST.in +0 -0
  26. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/README.rst +0 -0
  27. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/Makefile +0 -0
  28. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/make.bat +0 -0
  29. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/__init__.py +0 -0
  30. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/rtd_literal_block.css +0 -0
  31. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/rtd_theme_overrides.css +0 -0
  32. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/terminal_output.css +0 -0
  33. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/changelog.md +0 -0
  34. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/cli-commands.rst +0 -0
  35. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/cli-usage.rst +0 -0
  36. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/commands/dump_data_fields.rst +0 -0
  37. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/commands/fill_form.rst +0 -0
  38. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/conf.py +0 -0
  39. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/contributing.rst +0 -0
  40. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/index.rst +0 -0
  41. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/__init__.py +0 -0
  42. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/__main__.py +0 -0
  43. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/__init__.py +0 -0
  44. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/args.py +0 -0
  45. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/boolean_action.py +0 -0
  46. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/cli.py +0 -0
  47. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/command.py +0 -0
  48. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/__init__.py +0 -0
  49. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/fill_form.py +0 -0
  50. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/exit_codes.py +0 -0
  51. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/formatters.py +0 -0
  52. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/once_argument.py +0 -0
  53. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/smart_formatter.py +0 -0
  54. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/const.py +0 -0
  55. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/exceptions.py +0 -0
  56. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/__init__.py +0 -0
  57. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/colors.py +0 -0
  58. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/output.py +0 -0
  59. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/py.typed.py +0 -0
  60. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/utils.py +0 -0
  61. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/__init__.py +0 -0
  62. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/base.py +0 -0
  63. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/checkbox.py +0 -0
  64. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/radio.py +0 -0
  65. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/text.py +0 -0
  66. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/dependency_links.txt +0 -0
  67. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/entry_points.txt +0 -0
  68. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/top_level.txt +0 -0
  69. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-dev.txt +0 -0
  70. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-doc.txt +0 -0
  71. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-lint.txt +0 -0
  72. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-test.txt +0 -0
  73. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/setup.cfg +0 -0
  74. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/setup.py +0 -0
  75. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/__init__.py +0 -0
  76. {python_pdffiller-1.1.2/tests/unit → python_pdffiller-2.0.0/tests/cli}/__init__.py +0 -0
  77. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/data/empty.pdf +0 -0
  78. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/data/input.pdf +0 -0
  79. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_formatters.py +0 -0
  80. {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-pdffiller
3
- Version: 1.1.2
3
+ Version: 2.0.0
4
4
  Summary: Interact with PDF by inspecting or filling it
5
5
  Author-email: Jacques Raphanel <jraphanel@sismic.fr>
6
6
  License-Expression: MIT
@@ -26,7 +26,7 @@ Requires-Python: >=3.9
26
26
  Description-Content-Type: text/x-rst
27
27
  License-File: COPYING
28
28
  License-File: AUTHORS.rst
29
- Requires-Dist: pypdf
29
+ Requires-Dist: pymupdf==1.24.10
30
30
  Requires-Dist: colorama
31
31
  Requires-Dist: pyyaml
32
32
  Dynamic: license-file
@@ -3,4 +3,4 @@ __copyright__ = "Copyright 2025 SISMIC"
3
3
  __email__ = "jraphanel@sismic.fr"
4
4
  __license__ = "MIT"
5
5
  __title__ = "pdffiller"
6
- __version__ = "1.1.2"
6
+ __version__ = "2.0.0"
@@ -30,7 +30,8 @@ def dump_fields_text_formatter(pdf: Pdf) -> None:
30
30
 
31
31
  def dump_fields_json_formatter(pdf: Pdf) -> None:
32
32
  """Print output text for dump_fields command as simple text"""
33
-
33
+ if not pdf:
34
+ return
34
35
  cli_out_write(json.dumps(pdf.schema, indent=4, ensure_ascii=False))
35
36
 
36
37
 
@@ -0,0 +1,221 @@
1
+ """
2
+ A module for wrapping PDF form operations, providing a high-level interface
3
+ for filling, and manipulating PDF forms.
4
+
5
+ This module simplifies common tasks such as:
6
+ - Filling PDF forms with data from a dictionary.
7
+ - Fetching PDF forms fields
8
+
9
+ The core class, `Pdf`, encapsulates a PDF document and provides
10
+ methods for interacting with its form fields and content.
11
+ """
12
+
13
+ from collections import OrderedDict
14
+
15
+ import pymupdf
16
+
17
+ from pdffiller.exceptions import PdfFillerException
18
+ from pdffiller.io.output import PdfFillerOutput
19
+
20
+ from .typing import Any, cast, Dict, List, Optional, PathLike, StreamType, Type
21
+ from .widgets.base import Widget
22
+ from .widgets.checkbox import CheckBoxWidget
23
+ from .widgets.radio import RadioWidget
24
+ from .widgets.text import TextWidget
25
+
26
+
27
+ class PdfAttributes: # pylint: disable=too-few-public-methods
28
+ """Various constants, enums, and flags to aid readability."""
29
+
30
+ READ_ONLY = 1 << 0
31
+
32
+
33
+ class Pdf:
34
+ """
35
+ A class to wrap PDF form operations, providing a simplified interface
36
+ for common tasks such as filling, creating, and manipulating PDF forms.
37
+
38
+ The `Pdf` class encapsulates a PDF document and provides methods
39
+ for interacting with its form fields (widgets) and content.
40
+
41
+ """
42
+
43
+ TYPE_TO_OBJECT: Dict[str, Type[Widget]] = {
44
+ "Text": TextWidget,
45
+ "RadioButton": RadioWidget,
46
+ "CheckBox": CheckBoxWidget,
47
+ }
48
+
49
+ def __init__(
50
+ self, filename: Optional[PathLike] = None, stream: Optional[StreamType] = None
51
+ ) -> None:
52
+ """
53
+ Constructor method for the `Pdf` class.
54
+
55
+ Initializes a new `Pdf` object with the given template PDF and optional keyword arguments.
56
+
57
+ Args:
58
+ filename (Optional[PathLike]): Path to the input pdf
59
+ stream (Optional[StreamType]): An open file-like object containing the PDF data.
60
+ """
61
+
62
+ super().__init__()
63
+ self.widgets: OrderedDict[str, Widget] = OrderedDict()
64
+ self._init_helper(filename, stream)
65
+
66
+ def _init_helper(
67
+ self, filename: Optional[PathLike] = None, stream: Optional[StreamType] = None
68
+ ) -> None:
69
+ """
70
+ Helper method to initialize widgets
71
+
72
+ Args:
73
+ filename (Optional[PathLike]): Path to the input pdf
74
+ stream (Optional[StreamType]): An open file-like object containing the PDF data.
75
+ """
76
+ if not filename and not stream:
77
+ return
78
+
79
+ output = PdfFillerOutput()
80
+ output.info("loading file in memory")
81
+ loaded_widgets: OrderedDict[str, Widget] = OrderedDict()
82
+ try:
83
+ doc = pymupdf.open(filename=filename, stream=stream)
84
+ except Exception as ex: # pylint: disable=broad-exception-caught
85
+ PdfFillerOutput().error(str(ex))
86
+ raise PdfFillerException(
87
+ f"failed to load {filename or 'file from input string'}"
88
+ ) from ex
89
+
90
+ for i, page in enumerate(doc.pages()):
91
+ output.verbose(f"loading page {i+1}/{doc.page_count}")
92
+ for widget in page.widgets():
93
+ button_states = widget.button_states()
94
+ choices = button_states["normal"] if button_states else None
95
+
96
+ if widget.field_name not in loaded_widgets:
97
+ if widget.field_type_string not in self.TYPE_TO_OBJECT:
98
+ output.verbose(f"unsupported {widget.field_type_string} widget type")
99
+ continue
100
+ new_widget = self.TYPE_TO_OBJECT[widget.field_type_string](
101
+ widget.field_name, i, widget.field_value, widget.field_flags & (1 << 0)
102
+ )
103
+ if choices and isinstance(new_widget, CheckBoxWidget):
104
+ new_widget.choices = choices
105
+ elif isinstance(new_widget, TextWidget):
106
+ new_widget.max_length = widget.text_maxlen
107
+ loaded_widgets[widget.field_name] = new_widget
108
+ else:
109
+ new_widget = loaded_widgets[widget.field_name]
110
+ if choices and isinstance(new_widget, CheckBoxWidget):
111
+ for each in choices:
112
+ if new_widget.choices is not None:
113
+ if each not in new_widget.choices:
114
+ new_widget.choices.append(each)
115
+ else:
116
+ new_widget.choices = [each]
117
+
118
+ cast(CheckBoxWidget, loaded_widgets[widget.field_name]).choices = (
119
+ new_widget.choices
120
+ )
121
+
122
+ self.widgets = loaded_widgets
123
+
124
+ @property
125
+ def schema(self) -> List[Dict[str, Any]]:
126
+ """
127
+ Returns the JSON schema of the PDF form, describing the structure and data
128
+ types of the form fields.
129
+
130
+ This schema can be used to generate user interfaces or validate data before
131
+ filling the form.
132
+
133
+ Returns:
134
+ dict: A dictionary representing the JSON schema of the PDF form.
135
+ """
136
+
137
+ return [widget.schema_definition for widget in self.widgets.values()]
138
+
139
+ def fill(
140
+ self,
141
+ input_file: PathLike,
142
+ output_file: PathLike,
143
+ data: Dict[str, str],
144
+ flatten: bool = True,
145
+ ) -> "Pdf":
146
+ """
147
+ Fill the PDF form with data from a dictionary.
148
+
149
+ Args:
150
+ input_file (PathLike): The input file path.
151
+ output_file (PathLike): The output file path.
152
+ data (Dict[str, Union[str, bool, int]]): A dictionary where keys are form field names
153
+ and values are the data to fill the fields with. Values can be strings, booleans,
154
+ or integers.
155
+ flatten (bool): Whether to flatten the form after filling, making the fields read-only
156
+ (default: False).
157
+
158
+ Returns:
159
+ Pdf: The `Pdf` object, allowing for method chaining.
160
+ """
161
+ try:
162
+ document = pymupdf.open(filename=input_file)
163
+ except Exception as ex:
164
+ PdfFillerOutput().error(str(ex))
165
+ raise PdfFillerException(f"failed to open {input_file}") from ex
166
+
167
+ output = PdfFillerOutput()
168
+
169
+ output.info("filling pdf with input values")
170
+ # Iterate over all pages and process fields
171
+ for page in document:
172
+ for field in page.widgets():
173
+ if field.field_name in data:
174
+ value = data[field.field_name]
175
+
176
+ # Handling checkboxes
177
+ if (
178
+ field.field_type
179
+ == pymupdf.PDF_WIDGET_TYPE_CHECKBOX # pylint: disable=no-member
180
+ ):
181
+ print(f"{field.field_name} => {field.on_state()} vs {value}")
182
+ if value.strip() and "Off" != value.strip():
183
+ output.verbose(
184
+ f"updating checkbox with {value} from {field.field_value}"
185
+ )
186
+ field.field_value = True
187
+ else:
188
+ field.field_value = False
189
+
190
+ # Handling radio buttons
191
+ elif (
192
+ field.field_type
193
+ == pymupdf.PDF_WIDGET_TYPE_RADIOBUTTON # pylint: disable=no-member
194
+ ):
195
+ if value == field.on_state():
196
+ output.verbose(
197
+ f"updating radiobutton with {value} from {field.field_value}"
198
+ )
199
+ field.field_value = value
200
+
201
+ # Handling other fields types
202
+ else:
203
+ output.verbose(
204
+ f"updating {field.field_name} with {value} from {field.field_value}"
205
+ )
206
+ field.field_value = value
207
+
208
+ # Update the widget!
209
+ field.update()
210
+
211
+ try:
212
+ if flatten:
213
+ output.info("remove all annotations")
214
+ document.bake(annots=False)
215
+
216
+ # Save the modified PDF
217
+ document.save(output_file)
218
+ except Exception: # pylint: disable=broad-exception-caught
219
+ output.warning("an error occurs when saving file")
220
+
221
+ return self
@@ -43,7 +43,7 @@ __all__ = [
43
43
  "Type",
44
44
  "Union",
45
45
  "SubParserType",
46
- "StrByteType",
46
+ "StreamType",
47
47
  "TextIO",
48
48
  ]
49
49
 
@@ -52,7 +52,6 @@ ExitCode = Union[str, int, None]
52
52
  PathLike = Union[str, Path]
53
53
 
54
54
  StreamType = IO[Any]
55
- StrByteType = Union[PathLike, StreamType]
56
55
 
57
56
  if TYPE_CHECKING:
58
57
  # pylint: disable=protected-access,line-too-long
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-pdffiller"
7
- version = "1.1.2"
7
+ version = "2.0.0"
8
8
  description="Interact with PDF by inspecting or filling it"
9
9
  requires-python = ">=3.9"
10
10
  license ="MIT"
@@ -26,7 +26,7 @@ classifiers = [
26
26
  readme = "README.rst"
27
27
  authors = [{ name = "Jacques Raphanel", email = "jraphanel@sismic.fr" }]
28
28
  keywords = ["development", "pdf"]
29
- dependencies = ["pypdf", "colorama", "pyyaml"]
29
+ dependencies = ["pymupdf==1.24.10", "colorama", "pyyaml"]
30
30
 
31
31
  [project.scripts]
32
32
  pdffiller = "pdffiller.cli:main"
@@ -92,6 +92,7 @@ logging_use_named_masks = false
92
92
  major_on_zero = true
93
93
  tag_format = "v{version}"
94
94
  build_command = """
95
+ apt update && apt install -y --no-install-recommends pkg-config gcc g++ swig && \
95
96
  python -m pip install build -rrequirements.txt && \
96
97
  python -m build . && \
97
98
  python installer/build.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-pdffiller
3
- Version: 1.1.2
3
+ Version: 2.0.0
4
4
  Summary: Interact with PDF by inspecting or filling it
5
5
  Author-email: Jacques Raphanel <jraphanel@sismic.fr>
6
6
  License-Expression: MIT
@@ -26,7 +26,7 @@ Requires-Python: >=3.9
26
26
  Description-Content-Type: text/x-rst
27
27
  License-File: COPYING
28
28
  License-File: AUTHORS.rst
29
- Requires-Dist: pypdf
29
+ Requires-Dist: pymupdf==1.24.10
30
30
  Requires-Dist: colorama
31
31
  Requires-Dist: pyyaml
32
32
  Dynamic: license-file
@@ -48,7 +48,6 @@ pdffiller/cli/commands/dump_data_fields.py
48
48
  pdffiller/cli/commands/fill_form.py
49
49
  pdffiller/io/__init__.py
50
50
  pdffiller/io/colors.py
51
- pdffiller/io/custom_pdf_writer.py
52
51
  pdffiller/io/output.py
53
52
  pdffiller/widgets/__init__.py
54
53
  pdffiller/widgets/base.py
@@ -63,6 +62,9 @@ python_pdffiller.egg-info/requires.txt
63
62
  python_pdffiller.egg-info/top_level.txt
64
63
  tests/__init__.py
65
64
  tests/conftest.py
65
+ tests/cli/__init__.py
66
+ tests/cli/test_dump_data_field.py
67
+ tests/cli/test_fill_form.py
66
68
  tests/data/empty.pdf
67
69
  tests/data/input.pdf
68
70
  tests/unit/__init__.py
@@ -0,0 +1,3 @@
1
+ pymupdf==1.24.10
2
+ colorama
3
+ pyyaml
@@ -0,0 +1,3 @@
1
+ pymupdf==1.24.10
2
+ colorama
3
+ pyyaml
@@ -0,0 +1,106 @@
1
+ import json
2
+ from unittest import mock
3
+
4
+ import pytest
5
+
6
+ from pdffiller.cli import cli
7
+ from pdffiller.cli.exit_codes import (
8
+ ERROR_COMMAND_NAME,
9
+ ERROR_UNEXPECTED,
10
+ SUCCESS,
11
+ )
12
+
13
+
14
+ def test_incomplete_no_action():
15
+ """test empty command-line"""
16
+
17
+ with mock.patch("sys.argv", []):
18
+ assert cli.main() == ERROR_COMMAND_NAME
19
+
20
+
21
+ @pytest.mark.parametrize("argv", [])
22
+ def test_incomplete(argv):
23
+ """test empty command-line"""
24
+
25
+ # Test through direct command-line
26
+ with mock.patch("sys.argv", ["pdffiller", "dump_data_fields"] + argv):
27
+ assert cli.main() == ERROR_UNEXPECTED
28
+
29
+ # Test with direct call to main function
30
+ assert cli.main(["dump_data_fields"] + argv) == ERROR_UNEXPECTED
31
+
32
+
33
+ def test_complete(test_data_dir, capsys):
34
+ """test complete command-line"""
35
+
36
+ argv = [
37
+ str(test_data_dir / "input.pdf"),
38
+ ]
39
+
40
+ json_fields = """[
41
+ {
42
+ "FieldType": "text",
43
+ "FieldName": "Lastname"
44
+ },
45
+ {
46
+ "FieldType": "text",
47
+ "FieldName": "Firstname"
48
+ },
49
+ {
50
+ "FieldType": "checkbox",
51
+ "FieldOptions": [
52
+ "Off",
53
+ "Oui"
54
+ ],
55
+ "FieldName": "Men"
56
+ },
57
+ {
58
+ "FieldType": "checkbox",
59
+ "FieldOptions": [
60
+ "Off",
61
+ "Oui"
62
+ ],
63
+ "FieldName": "Women"
64
+ },
65
+ {
66
+ "FieldType": "radio",
67
+ "FieldOptions": [
68
+ "Off",
69
+ "Single",
70
+ "Married",
71
+ "Divorced"
72
+ ],
73
+ "FieldName": "MaritalStatus",
74
+ "FieldValue": "Off"
75
+ }
76
+ ]"""
77
+ # Test through direct command-line
78
+ with mock.patch(
79
+ "sys.argv",
80
+ ["pdffiller", "dump_data_fields", "-fjson"] + argv,
81
+ ):
82
+ assert cli.main() == SUCCESS
83
+ out, err = capsys.readouterr()
84
+ assert "loading file in memory" == err.strip()
85
+ assert json.loads(json_fields) == json.loads(out)
86
+
87
+ # Test with direct call to main function
88
+ assert cli.main(["dump_data_fields"] + argv) == SUCCESS
89
+
90
+
91
+ def test_complete_with_invalid_file(test_data_dir):
92
+ """test complete command-line"""
93
+
94
+ argv = [
95
+ str(test_data_dir / "empty.pdf"),
96
+ ]
97
+
98
+ # Test through direct command-line
99
+ with mock.patch(
100
+ "sys.argv",
101
+ ["pdffiller", "dump_data_fields"] + argv,
102
+ ):
103
+ assert cli.main() == ERROR_UNEXPECTED
104
+
105
+ # Test with direct call to main function
106
+ assert cli.main(["dump_data_fields"] + argv) == ERROR_UNEXPECTED
@@ -0,0 +1,103 @@
1
+ import json
2
+ from unittest import mock
3
+
4
+ import pytest
5
+
6
+ from pdffiller.cli import cli
7
+ from pdffiller.cli.exit_codes import (
8
+ ERROR_COMMAND_NAME,
9
+ ERROR_UNEXPECTED,
10
+ SUCCESS,
11
+ )
12
+
13
+
14
+ def test_incomplete_no_action():
15
+ """test empty command-line"""
16
+
17
+ with mock.patch("sys.argv", []):
18
+ assert cli.main() == ERROR_COMMAND_NAME
19
+
20
+
21
+ @pytest.mark.parametrize("argv", [])
22
+ def test_incomplete(argv):
23
+ """test empty command-line"""
24
+
25
+ # Test through direct command-line
26
+ with mock.patch("sys.argv", ["pdffiller", "fill_form"] + argv):
27
+ assert cli.main() == ERROR_UNEXPECTED
28
+
29
+ # Test with direct call to main function
30
+ assert cli.main(["fill_form"] + argv) == ERROR_UNEXPECTED
31
+
32
+
33
+ def test_complete_with_files_only(test_data_dir, input_json_fields, output_pdf_path):
34
+ """test complete command-line"""
35
+
36
+ argv = [
37
+ "-d",
38
+ str(input_json_fields),
39
+ "-o",
40
+ str(output_pdf_path),
41
+ str(test_data_dir / "input.pdf"),
42
+ ]
43
+
44
+ # Test through direct command-line
45
+ with mock.patch(
46
+ "sys.argv",
47
+ ["pdffiller", "fill_form"] + argv,
48
+ ):
49
+ assert cli.main() == SUCCESS
50
+ assert output_pdf_path.exists()
51
+ output_pdf_path.unlink()
52
+
53
+ # Test with direct call to main function
54
+ assert cli.main(["fill_form"] + argv) == SUCCESS
55
+ assert output_pdf_path.exists()
56
+
57
+
58
+ def test_complete_with_invalid_file(test_data_dir, input_json_fields, output_pdf_path):
59
+ """test complete command-line"""
60
+
61
+ argv = [
62
+ "-d",
63
+ input_json_fields,
64
+ "-o",
65
+ output_pdf_path,
66
+ str(test_data_dir / "empty.pdf"),
67
+ ]
68
+
69
+ # Test through direct command-line
70
+ with mock.patch(
71
+ "sys.argv",
72
+ ["pdffiller", "fill_form"] + argv,
73
+ ):
74
+ assert cli.main() == ERROR_UNEXPECTED
75
+
76
+ # Test with direct call to main function
77
+ assert cli.main(["fill_form"] + argv) == ERROR_UNEXPECTED
78
+
79
+
80
+ def test_complete_with_file_and_string(test_data_dir, input_json_fields, output_pdf_path):
81
+ """test complete command-line"""
82
+
83
+ json_fields = input_json_fields.read_text()
84
+ argv = [
85
+ "-i",
86
+ json_fields,
87
+ "-o",
88
+ str(output_pdf_path),
89
+ str(test_data_dir / "input.pdf"),
90
+ ]
91
+
92
+ # Test through direct command-line
93
+ with mock.patch(
94
+ "sys.argv",
95
+ ["pdffiller", "fill_form"] + argv,
96
+ ):
97
+ assert cli.main() == SUCCESS
98
+ assert output_pdf_path.exists()
99
+ output_pdf_path.unlink()
100
+
101
+ # Test with direct call to main function
102
+ assert cli.main(["fill_form"] + argv) == SUCCESS
103
+ assert output_pdf_path.exists()
@@ -0,0 +1,70 @@
1
+ import shutil
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ import time
7
+ from pdffiller.typing import Generator
8
+
9
+
10
+ @pytest.fixture(scope="session")
11
+ def test_dir(pytestconfig) -> Path:
12
+ """Retrieve path to tests directory from test environment"""
13
+ return pytestconfig.rootdir / "tests"
14
+
15
+
16
+ @pytest.fixture(scope="session")
17
+ def test_data_dir(pytestconfig) -> Path:
18
+ """Retrieve path to tests data directory from test environment"""
19
+ return pytestconfig.rootdir / "tests" / "data"
20
+
21
+
22
+ @pytest.fixture(name="tmp_output_dir")
23
+ def fixture_tmp_output_dir(tmp_path) -> Generator[Path, None, None]:
24
+ """Generate temporary history file"""
25
+ local_output_dir = tmp_path / "_output"
26
+ local_output_dir.mkdir(parents=True)
27
+ yield local_output_dir
28
+ shutil.rmtree(local_output_dir)
29
+
30
+
31
+ @pytest.fixture(name="input_json_fields")
32
+ def fixture_dynamic_config_file(tmp_path) -> Generator[Path, None, None]:
33
+ """Generate temporary input json file"""
34
+ config_path = tmp_path / "input.json"
35
+
36
+ json_content = """[
37
+ {
38
+ "name": "Lastname",
39
+ "value": "Doe"
40
+ },
41
+ {
42
+ "name": "Firstname",
43
+ "value": "John"
44
+ },
45
+ {
46
+ "name": "Men",
47
+ "value": "On"
48
+ },
49
+ {
50
+ "name": "Women",
51
+ "value": ""
52
+ },
53
+ {
54
+ "name": "MaritalStatus",
55
+ "value": "Single"
56
+ }
57
+ ]
58
+ """
59
+ config_path.write_text(json_content)
60
+ yield config_path
61
+ config_path.unlink()
62
+
63
+
64
+ @pytest.fixture(name="output_pdf_path")
65
+ def fixture_output_pdf_path(tmp_path) -> Generator[Path, None, None]:
66
+ """Generate temporary output pdf file"""
67
+ output_path = tmp_path / f"{time.time()}.pdf"
68
+ yield output_path
69
+ if output_path.exists():
70
+ output_path.unlink()
File without changes
@@ -1,6 +1,7 @@
1
1
  import pytest
2
2
 
3
3
  from pdffiller.pdf import Pdf
4
+ from pdffiller.exceptions import PdfFillerException
4
5
 
5
6
 
6
7
  def test_valid_pdf(test_data_dir):
@@ -14,11 +15,11 @@ def test_valid_pdf(test_data_dir):
14
15
  assert schema[3]["FieldName"] == "Women"
15
16
  assert schema[4]["FieldName"] == "MaritalStatus"
16
17
  assert len(schema[4]["FieldOptions"]) == 4
17
- assert schema[4]["FieldOptions"][0] == "Divorced"
18
- assert schema[4]["FieldOptions"][1] == "Off"
19
- assert schema[4]["FieldValue"] == "Married"
18
+ assert "Divorced" in schema[4]["FieldOptions"]
19
+ assert "Off" in schema[4]["FieldOptions"]
20
+ assert schema[4]["FieldValue"] == "Off"
20
21
 
21
22
 
22
23
  def test_invalid_pdf(test_data_dir):
23
- reader = Pdf(str(test_data_dir / "empty.pdf"))
24
- assert len(reader.widgets) == 0
24
+ with pytest.raises(PdfFillerException):
25
+ Pdf(str(test_data_dir / "empty.pdf"))