proof-pdf 0.3.2__tar.gz → 0.3.3__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 (83) hide show
  1. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/PKG-INFO +7 -1
  2. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/README.md +6 -0
  3. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/pyproject.toml +1 -1
  4. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/__init__.py +8 -3
  5. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/cli.py +30 -24
  6. proof_pdf-0.3.3/src/proof_pdf/config.py +79 -0
  7. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/core.py +10 -0
  8. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_core.py +15 -0
  9. proof_pdf-0.3.3/tests/test_proof_pdf/test_load_profile.py +90 -0
  10. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_public_api.py +1 -1
  11. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/LICENSE +0 -0
  12. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/NOTICE +0 -0
  13. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/__init__.py +0 -0
  14. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/colour.py +0 -0
  15. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/config.py +0 -0
  16. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/content.py +0 -0
  17. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/contrast.py +0 -0
  18. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/deltae.py +0 -0
  19. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/fonts.py +0 -0
  20. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/paint.py +0 -0
  21. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/proc.py +0 -0
  22. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/resolution.py +0 -0
  23. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/spaces.py +0 -0
  24. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/structure.py +0 -0
  25. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/text.py +0 -0
  26. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/_internal/verapdf.py +0 -0
  27. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/inventory.py +0 -0
  28. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/models.py +0 -0
  29. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/preview.py +0 -0
  30. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/py.typed +0 -0
  31. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/src/proof_pdf/texlog.py +0 -0
  32. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/build_pdfs.py +0 -0
  33. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/black-text.pdf +0 -0
  34. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/corrupt.pdf +0 -0
  35. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/cyan-text.pdf +0 -0
  36. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/edge-text.pdf +0 -0
  37. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/embedded-text.pdf +0 -0
  38. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/good.pdf +0 -0
  39. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/hairline.pdf +0 -0
  40. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/heavy-ink.pdf +0 -0
  41. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/high-contrast.pdf +0 -0
  42. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/low-contrast.pdf +0 -0
  43. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/low-dpi-image.pdf +0 -0
  44. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/no-bleed.pdf +0 -0
  45. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/no-output-intent.pdf +0 -0
  46. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/notdef-text.pdf +0 -0
  47. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/plain.pdf +0 -0
  48. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/registration-spot.pdf +0 -0
  49. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/rgb-image.pdf +0 -0
  50. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/rich-black-text.pdf +0 -0
  51. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/small-text.pdf +0 -0
  52. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/spot-ink.pdf +0 -0
  53. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/three-pages.pdf +0 -0
  54. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/transparent-matching.pdf +0 -0
  55. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/transparent-mismatch.pdf +0 -0
  56. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/transparent-undeclared.pdf +0 -0
  57. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/two-sizes.pdf +0 -0
  58. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/type3-text.pdf +0 -0
  59. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/white-overprint.pdf +0 -0
  60. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/pdfs/wrong-size.pdf +0 -0
  61. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/suites/README.md +0 -0
  62. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/fixtures/suites/expected.toml +0 -0
  63. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/__init__.py +0 -0
  64. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/conftest.py +0 -0
  65. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_bleed.py +0 -0
  66. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_cli.py +0 -0
  67. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_colour.py +0 -0
  68. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_config.py +0 -0
  69. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_content.py +0 -0
  70. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_contrast.py +0 -0
  71. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_deltae.py +0 -0
  72. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_fixtures_current.py +0 -0
  73. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_integration.py +0 -0
  74. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_inventory.py +0 -0
  75. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_paint.py +0 -0
  76. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_preview.py +0 -0
  77. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_proc.py +0 -0
  78. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_resolution.py +0 -0
  79. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_structure.py +0 -0
  80. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_suites.py +0 -0
  81. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_texlog.py +0 -0
  82. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_text.py +0 -0
  83. {proof_pdf-0.3.2 → proof_pdf-0.3.3}/tests/test_proof_pdf/test_verapdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proof-pdf
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Hold print PDFs to PDF/X-4 and PDF/A: veraPDF, page boxes, geometry, ink and image resolution.
5
5
  Keywords: pdf,pdf/x-4,pdf/a,verapdf,prepress,print,preflight,ghostscript,lualatex
6
6
  Author-Email: Ilja Orlovs <ilja@release.art>
@@ -79,6 +79,12 @@ so nothing blocks the event loop.
79
79
  As a library the API is async: `await proof_pdf.core.verify_file(path, profile)` returns a `FileReport` of `Finding`s, and
80
80
  `await proof_pdf.core.verify_files(paths, profile, jobs=4)` returns one report per path in order. See `proof_pdf.models.Profile`.
81
81
 
82
+ A job's config file works from Python too. `proof_pdf.load_profile(path=None, *, profile=None)` reads it like the CLI does (the nearest
83
+ `proof-pdf.toml`, `PROOF_PDF_CONFIG`, `PROOF_PDF_PROFILE`) and returns a `LoadedConfig`: the `Profile`, the `exclude` globs, the `exempt`
84
+ map, and `jobs`, `timeout` and the tool names. A file the CLI would reject raises `proof_pdf.ConfigError`. Pass those to
85
+ `core.expand_targets` and `core.verify_files`, so the CLI and your code share one definition. `core.missing_tools(profile)` names the
86
+ tools the profile needs (veraPDF for PDF/A, Ghostscript for colour) that are not on `PATH`, so a caller can fail early with one message.
87
+
82
88
  ### Proofing
83
89
 
84
90
  Beyond the standards, `verify` can hold a PDF to the rules a print job usually has. Missing glyphs and fonts are checked by
@@ -53,6 +53,12 @@ so nothing blocks the event loop.
53
53
  As a library the API is async: `await proof_pdf.core.verify_file(path, profile)` returns a `FileReport` of `Finding`s, and
54
54
  `await proof_pdf.core.verify_files(paths, profile, jobs=4)` returns one report per path in order. See `proof_pdf.models.Profile`.
55
55
 
56
+ A job's config file works from Python too. `proof_pdf.load_profile(path=None, *, profile=None)` reads it like the CLI does (the nearest
57
+ `proof-pdf.toml`, `PROOF_PDF_CONFIG`, `PROOF_PDF_PROFILE`) and returns a `LoadedConfig`: the `Profile`, the `exclude` globs, the `exempt`
58
+ map, and `jobs`, `timeout` and the tool names. A file the CLI would reject raises `proof_pdf.ConfigError`. Pass those to
59
+ `core.expand_targets` and `core.verify_files`, so the CLI and your code share one definition. `core.missing_tools(profile)` names the
60
+ tools the profile needs (veraPDF for PDF/A, Ghostscript for colour) that are not on `PATH`, so a caller can fail early with one message.
61
+
56
62
  ### Proofing
57
63
 
58
64
  Beyond the standards, `verify` can hold a PDF to the rules a print job usually has. Missing glyphs and fonts are checked by
@@ -37,7 +37,7 @@ dependencies = [
37
37
  "pillow>=12.3.0",
38
38
  "typer>=0.27.3",
39
39
  ]
40
- version = "0.3.2"
40
+ version = "0.3.3"
41
41
 
42
42
  [project.urls]
43
43
  Homepage = "https://github.com/release-art/proofpress"
@@ -8,11 +8,16 @@ coverage, overprint, rich-black small text, stray RGB, effective image DPI).
8
8
  The entry point is :func:`proof_pdf.core.verify_file`; the CLI group lives in
9
9
  :mod:`proof_pdf.cli` and is the ``proof-pdf`` console script.
10
10
 
11
- Public API is the top-level modules: :mod:`~proof_pdf.core`, :mod:`~proof_pdf.models`, :mod:`~proof_pdf.inventory`,
12
- :mod:`~proof_pdf.preview`, :mod:`~proof_pdf.texlog` and :mod:`~proof_pdf.cli`. Everything under ``proof_pdf._internal``
13
- (the checks, the content and paint walks, the tool wrappers, the config reader) is private and may change at any time.
11
+ Public API is the top-level modules (:mod:`~proof_pdf.core`, :mod:`~proof_pdf.models`, :mod:`~proof_pdf.inventory`,
12
+ :mod:`~proof_pdf.preview`, :mod:`~proof_pdf.texlog`, :mod:`~proof_pdf.config`, :mod:`~proof_pdf.cli`) and the
13
+ :func:`~proof_pdf.config.load_profile` re-export. Everything under ``proof_pdf._internal`` (the checks, the content and
14
+ paint walks, the tool wrappers, the config reader) is private and may change at any time.
14
15
 
15
16
  The package takes PDF paths and nothing else. The finished-page geometry it checks
16
17
  against is a parameter (:class:`PageGeometry`) with no default, so a file from a
17
18
  printer is held to its own dimensions or the layer is skipped.
18
19
  """
20
+
21
+ from .config import ConfigError, LoadedConfig, load_profile
22
+
23
+ __all__ = ["ConfigError", "LoadedConfig", "load_profile"]
@@ -228,30 +228,7 @@ def verify_cmd(
228
228
  typer.echo("no PDFs to verify", err=True)
229
229
  raise typer.Exit(1)
230
230
 
231
- profile = Profile(
232
- flavour=flavour,
233
- pdfa=pdfa,
234
- pdfx=pdfx,
235
- colour=colour,
236
- geometry=_page_geometry(trim_mm, bleed_mm, tolerance_mm) if trim_mm else None,
237
- max_tac=max_tac,
238
- small_text_pt=small_text_pt,
239
- min_dpi=min_dpi,
240
- strict_cmyk=strict_cmyk,
241
- k_only_text=k_only_text,
242
- missing_glyphs=missing_glyphs,
243
- min_text_pt=min_text_pt,
244
- min_stroke_pt=min_stroke_pt,
245
- safe_mm=safe_mm,
246
- min_delta_l=min_delta_l,
247
- proof_profile=proof_profile,
248
- fonts=fonts,
249
- allow_spots=tuple(allow_spot) if allow_spot else None,
250
- output_condition=output_condition,
251
- require_bleed=require_bleed,
252
- uniform_pages=uniform_pages,
253
- pages_multiple_of=pages_multiple_of,
254
- )
231
+ profile = build_profile(locals())
255
232
  reports = asyncio.run(
256
233
  core.verify_files(
257
234
  targets,
@@ -402,6 +379,35 @@ def _parse_exemptions(specs: list[str]) -> dict[str, str]:
402
379
  return exemptions
403
380
 
404
381
 
382
+ def build_profile(options: typing.Mapping[str, typing.Any]) -> Profile:
383
+ """The :class:`Profile` the ``verify`` options (by parameter name) describe; also used by ``load_profile``."""
384
+ o = options
385
+ return Profile(
386
+ flavour=o["flavour"],
387
+ pdfa=o["pdfa"],
388
+ pdfx=o["pdfx"],
389
+ colour=o["colour"],
390
+ geometry=_page_geometry(o["trim_mm"], o["bleed_mm"], o["tolerance_mm"]) if o["trim_mm"] else None,
391
+ max_tac=o["max_tac"],
392
+ small_text_pt=o["small_text_pt"],
393
+ min_dpi=o["min_dpi"],
394
+ strict_cmyk=o["strict_cmyk"],
395
+ k_only_text=o["k_only_text"],
396
+ missing_glyphs=o["missing_glyphs"],
397
+ min_text_pt=o["min_text_pt"],
398
+ min_stroke_pt=o["min_stroke_pt"],
399
+ safe_mm=o["safe_mm"],
400
+ min_delta_l=o["min_delta_l"],
401
+ proof_profile=o["proof_profile"],
402
+ fonts=o["fonts"],
403
+ allow_spots=tuple(o["allow_spot"]) if o["allow_spot"] else None,
404
+ output_condition=o["output_condition"],
405
+ require_bleed=o["require_bleed"],
406
+ uniform_pages=o["uniform_pages"],
407
+ pages_multiple_of=o["pages_multiple_of"],
408
+ )
409
+
410
+
405
411
  def _page_geometry(trim_mm: str, bleed_mm: float, tolerance_mm: float) -> PageGeometry:
406
412
  """Parse ``--trim-mm WxH`` into a :class:`PageGeometry`."""
407
413
  parts = trim_mm.lower().replace("*", "x").split("x")
@@ -0,0 +1,79 @@
1
+ """Read a ``proof-pdf.toml`` the way the CLI does, for library callers.
2
+
3
+ One file can then carry a printer's numbers for both ``proof-pdf verify`` and Python code that calls
4
+ :func:`proof_pdf.core.verify_files`.
5
+ """
6
+
7
+ import dataclasses
8
+ import os
9
+ import pathlib
10
+ import typing
11
+
12
+ import typer
13
+ import typer.main
14
+
15
+ from ._internal import config as _config
16
+ from ._internal.config import ConfigError
17
+ from .models import Profile
18
+
19
+ __all__ = ["ConfigError", "LoadedConfig", "load_profile"]
20
+
21
+
22
+ @dataclasses.dataclass(frozen=True)
23
+ class LoadedConfig:
24
+ """What ``verify`` would run with: the checks and the file selection, from the file plus the option defaults."""
25
+
26
+ profile: Profile
27
+ exclude: tuple[str, ...] # globs for :func:`proof_pdf.core.expand_targets`
28
+ exempt: dict[str, str] # glob -> reason, for ``verify_files(exemptions=...)``
29
+ jobs: int
30
+ timeout: float
31
+ verapdf_bin: str
32
+ gs_bin: str
33
+ path: pathlib.Path | None # the file that was read; None when none was found
34
+
35
+
36
+ def load_profile(
37
+ path: pathlib.Path | None = None, *, profile: str | None = None, command: str = "verify"
38
+ ) -> LoadedConfig:
39
+ """The settings the CLI's *command* would use, read from the config file.
40
+
41
+ *path* defaults to ``PROOF_PDF_CONFIG``, else the nearest ``proof-pdf.toml`` (or ``[tool.proof-pdf]`` in
42
+ ``pyproject.toml``) from the working directory; *profile* defaults to ``PROOF_PDF_PROFILE``. With no file, every
43
+ option has its CLI default. Raises :class:`ConfigError` for an unreadable file, an unknown key or table, a profile
44
+ the file lacks, or a value the CLI would reject.
45
+ """
46
+ from . import cli # deferred: the CLI framework is only loaded when a caller wants its behaviour
47
+
48
+ if command != "verify":
49
+ raise ConfigError(f"only the verify command makes a profile, not {command!r}")
50
+ path = path or (pathlib.Path(env) if (env := os.environ.get("PROOF_PDF_CONFIG")) else None)
51
+ profile = profile or os.environ.get("PROOF_PDF_PROFILE") or None
52
+ path = path or _config.find_config(pathlib.Path.cwd())
53
+ if path is None and profile:
54
+ raise ConfigError("a profile needs a config file, and none was found")
55
+ commands = _click_commands()
56
+ defaults = _config.default_map(path, profile, commands) if path else {}
57
+ try:
58
+ with commands[command].make_context(command, [], default_map=defaults.get(command, {})) as ctx:
59
+ values = ctx.params
60
+ built = cli.build_profile(values)
61
+ exempt = cli._parse_exemptions(values["exempt"] or [])
62
+ except typer.BadParameter as exc: # a bad value is the file's fault, not a CLI usage error
63
+ raise ConfigError(f"{path or 'defaults'}: {exc.format_message()}") from exc
64
+ return LoadedConfig(
65
+ profile=built,
66
+ exclude=tuple(values["exclude"] or ()),
67
+ exempt=exempt,
68
+ jobs=values["jobs"],
69
+ timeout=values["timeout"],
70
+ verapdf_bin=values["verapdf_bin"],
71
+ gs_bin=values["gs_bin"],
72
+ path=path,
73
+ )
74
+
75
+
76
+ def _click_commands() -> dict[str, typing.Any]:
77
+ from . import cli
78
+
79
+ return dict(getattr(typer.main.get_command(cli.pdf_app), "commands", {}))
@@ -56,6 +56,16 @@ async def verify_file(
56
56
  return report
57
57
 
58
58
 
59
+ def missing_tools(profile: Profile, *, verapdf_bin: str = "verapdf", gs_bin: str = "gs") -> list[str]:
60
+ """The external tools *profile* needs that cannot be found, by the executable names given (empty if all are there).
61
+
62
+ veraPDF is needed for the PDF/A layer and Ghostscript for the colour layer (total ink). :func:`verify_file` reports
63
+ a missing tool as a failing finding; this lets a caller stop before a long run with one message of its own.
64
+ """
65
+ needed = ((verapdf_bin, profile.pdfa, verapdf.available), (gs_bin, profile.colour, colour.gs_available))
66
+ return [binary for binary, wanted, found in needed if wanted and not found(binary)]
67
+
68
+
59
69
  def exemption(path: pathlib.PurePath, exemptions: collections.abc.Mapping[str, str]) -> str | None:
60
70
  """The reason *path* was declared exempt, or ``None``.
61
71
 
@@ -259,3 +259,18 @@ class TestFileReport:
259
259
  )
260
260
  def test_finding_label(self, page, expected):
261
261
  assert Finding("x", Severity.ERROR, "hello", page).label() == expected
262
+
263
+
264
+ class TestMissingTools:
265
+ def test_names_only_what_the_profile_needs_and_cannot_find(self, mocker):
266
+ mocker.patch.object(verapdf, "available", return_value=False)
267
+ mocker.patch.object(colour, "gs_available", return_value=False)
268
+ assert core.missing_tools(Profile(), verapdf_bin="vera", gs_bin="ghost") == ["vera", "ghost"]
269
+ assert core.missing_tools(Profile(pdfa=False)) == ["gs"]
270
+ assert core.missing_tools(Profile(colour=False)) == ["verapdf"]
271
+ assert core.missing_tools(Profile(pdfa=False, colour=False)) == []
272
+
273
+ def test_a_tool_that_is_there_is_not_reported(self, mocker):
274
+ mocker.patch.object(verapdf, "available", return_value=True)
275
+ mocker.patch.object(colour, "gs_available", return_value=True)
276
+ assert core.missing_tools(Profile()) == []
@@ -0,0 +1,90 @@
1
+ """``proof_pdf.load_profile``: the CLI's config reading, for library callers."""
2
+
3
+ import pytest
4
+
5
+ import proof_pdf
6
+ from proof_pdf.models import PageGeometry, Profile
7
+
8
+ CARD = """
9
+ [verify]
10
+ trim-mm = "105x148"
11
+ bleed-mm = 3
12
+ exclude = ["res/**", "_views/**", "*_preview.pdf"]
13
+ safe-mm = 3
14
+ require-bleed = true
15
+
16
+ [verify.exempt]
17
+ "manual.pdf" = "internal"
18
+
19
+ [profiles.deluxe.verify]
20
+ bleed-mm = 5
21
+ """
22
+
23
+
24
+ @pytest.fixture
25
+ def card(tmp_path, monkeypatch):
26
+ monkeypatch.delenv("PROOF_PDF_CONFIG", raising=False)
27
+ monkeypatch.delenv("PROOF_PDF_PROFILE", raising=False)
28
+ path = tmp_path / "proof-pdf.toml"
29
+ path.write_text(CARD)
30
+ return path
31
+
32
+
33
+ def test_the_file_gives_the_profile_the_globs_and_the_exemptions(card):
34
+ loaded = proof_pdf.load_profile(card)
35
+ assert loaded.profile.geometry == PageGeometry(trim_w_mm=105, trim_h_mm=148, bleed_mm=3)
36
+ assert loaded.profile.safe_mm == 3 and loaded.profile.require_bleed is True
37
+ assert loaded.exclude == ("res/**", "_views/**", "*_preview.pdf")
38
+ assert loaded.exempt == {"manual.pdf": "internal"} and loaded.path == card
39
+
40
+
41
+ def test_a_named_profile_overrides_the_base(card):
42
+ assert proof_pdf.load_profile(card, profile="deluxe").profile.geometry.bleed_mm == 5 # type: ignore[union-attr]
43
+
44
+
45
+ def test_the_nearest_file_is_found_from_the_working_directory(card, monkeypatch):
46
+ monkeypatch.chdir(card.parent)
47
+ assert proof_pdf.load_profile().path == card
48
+
49
+
50
+ def test_the_environment_names_the_file_and_profile(card, monkeypatch, tmp_path):
51
+ monkeypatch.chdir(tmp_path / "..")
52
+ monkeypatch.setenv("PROOF_PDF_CONFIG", str(card))
53
+ monkeypatch.setenv("PROOF_PDF_PROFILE", "deluxe")
54
+ assert proof_pdf.load_profile().profile.geometry.bleed_mm == 5 # type: ignore[union-attr]
55
+
56
+
57
+ def test_no_file_means_the_cli_defaults(tmp_path, monkeypatch):
58
+ monkeypatch.delenv("PROOF_PDF_CONFIG", raising=False)
59
+ monkeypatch.chdir(tmp_path)
60
+ loaded = proof_pdf.load_profile()
61
+ assert loaded.profile == Profile() and loaded.path is None and loaded.exclude == () and loaded.exempt == {}
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "text",
66
+ [
67
+ pytest.param("[verify]\nnot-an-option = 1\n", id="unknown-key"),
68
+ pytest.param('[verify]\ntrim-mm = "wide"\n', id="bad-trim"),
69
+ pytest.param('[verify]\nmin-dpi = "high"\n', id="bad-type"),
70
+ pytest.param("[nope]\nx = 1\n", id="unknown-table"),
71
+ ],
72
+ )
73
+ def test_a_file_the_cli_would_reject_is_a_config_error(tmp_path, monkeypatch, text):
74
+ monkeypatch.delenv("PROOF_PDF_CONFIG", raising=False)
75
+ path = tmp_path / "proof-pdf.toml"
76
+ path.write_text(text)
77
+ with pytest.raises(proof_pdf.ConfigError):
78
+ proof_pdf.load_profile(path)
79
+
80
+
81
+ def test_a_profile_without_a_file_is_a_config_error(tmp_path, monkeypatch):
82
+ monkeypatch.delenv("PROOF_PDF_CONFIG", raising=False)
83
+ monkeypatch.chdir(tmp_path)
84
+ with pytest.raises(proof_pdf.ConfigError):
85
+ proof_pdf.load_profile(profile="deluxe")
86
+
87
+
88
+ def test_only_verify_makes_a_profile():
89
+ with pytest.raises(proof_pdf.ConfigError):
90
+ proof_pdf.load_profile(command="inspect")
@@ -4,7 +4,7 @@ import pkgutil
4
4
 
5
5
  import proof_pdf
6
6
 
7
- PUBLIC = {"cli", "core", "inventory", "models", "preview", "texlog"}
7
+ PUBLIC = {"cli", "config", "core", "inventory", "models", "preview", "texlog"}
8
8
 
9
9
 
10
10
  def test_only_the_public_modules_sit_at_the_top_level():
File without changes
File without changes