Skip to content

Commit 60e1d1f

Browse files
committed
Remove circular dependency to jsonschema :(
1 parent ff1b156 commit 60e1d1f

File tree

9 files changed

+96
-333
lines changed

9 files changed

+96
-333
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ The **third number** is for emergencies when we need to start branches for older
1313

1414
## [Unreleased](https://github.com/hynek/hatch-fancy-pypi-readme/compare/22.6.0...HEAD)
1515

16+
### Changed
17+
18+
- Removed another circular dependency: this time the wonderful [*jsonschema*](https://python-jsonschema.readthedocs.io/).
19+
The price of building packaging tools is to not use packages.
20+
1621

1722
## [22.6.0](https://github.com/hynek/hatch-fancy-pypi-readme/compare/22.5.0...22.6.0) - 2022-09-11
1823

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ classifiers = [
2929
]
3030
dependencies = [
3131
"hatchling",
32-
"jsonschema",
3332
"tomli; python_version<'3.11'",
3433
"typing-extensions; python_version<'3.8'",
3534
]

src/hatch_fancy_pypi_readme/_config.py

Lines changed: 43 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@
55
from __future__ import annotations
66

77
from dataclasses import dataclass
8-
from typing import Any
9-
10-
import jsonschema
8+
from typing import Any, cast
119

1210
from ._fragments import VALID_FRAGMENTS, Fragment
13-
from ._humanize_validation_errors import errors_to_human_strings
1411
from ._substitutions import Substituter
15-
from ._validators import CustomValidator
1612
from .exceptions import ConfigurationError
1713

1814

@@ -23,91 +19,72 @@ class Config:
2319
substitutions: list[Substituter]
2420

2521

26-
SCHEMA = {
27-
"$schema": CustomValidator.META_SCHEMA["$id"],
28-
"type": "object",
29-
"properties": {
30-
"content-type": {
31-
"type": "string",
32-
"enum": ["text/markdown", "text/x-rst"],
33-
},
34-
"fragments": {
35-
"type": "array",
36-
"minItems": 1,
37-
# Items are validated separately for better error messages.
38-
"items": {"type": "object"},
39-
},
40-
"substitutions": {
41-
"type": "array",
42-
"items": {
43-
"type": "object",
44-
"properties": {
45-
"pattern": {"type": "string", "regex": True},
46-
"replacement": {"type": "string"},
47-
"ignore-case": {"type": "boolean"},
48-
},
49-
"required": ["pattern", "replacement"],
50-
"additionalProperties": False,
51-
},
52-
},
53-
},
54-
"required": ["content-type", "fragments"],
55-
"additionalProperties": False,
56-
}
22+
_BASE = "tool.hatch.metadata.hooks.fancy-pypi-readme."
5723

5824

5925
def load_and_validate_config(config: dict[str, Any]) -> Config:
60-
errs = sorted(
61-
CustomValidator(SCHEMA).iter_errors(config),
62-
key=jsonschema.exceptions.relevance,
63-
)
26+
errs = []
27+
28+
ct = config.get("content-type")
29+
if ct is None:
30+
errs.append(f"{_BASE}content-type is missing.")
31+
elif ct not in ("text/markdown", "text/x-rst"):
32+
errs.append(
33+
f"{_BASE}content-type: '{ct}' is not one of "
34+
"['text/markdown', 'text/x-rst']"
35+
)
36+
37+
try:
38+
fragments = _load_fragments(config.get("fragments"))
39+
except ConfigurationError as e:
40+
errs.extend(e.errors)
41+
42+
try:
43+
subs_cfg = config.get("substitutions", [])
44+
if not isinstance(subs_cfg, list):
45+
raise ConfigurationError(
46+
[f"{_BASE}substitutions must be an array."]
47+
)
48+
49+
substitutions = [
50+
Substituter.from_config(sub_cfg) for sub_cfg in subs_cfg
51+
]
52+
except ConfigurationError as e:
53+
errs.extend(e.errors)
54+
6455
if errs:
65-
raise ConfigurationError(errors_to_human_strings(errs))
56+
raise ConfigurationError(errs)
6657

6758
return Config(
68-
config["content-type"],
69-
_load_fragments(config["fragments"]),
70-
[
71-
Substituter.from_config(sub_cfg)
72-
for sub_cfg in config.get("substitutions", [])
73-
],
59+
content_type=cast(str, ct),
60+
fragments=fragments,
61+
substitutions=substitutions,
7462
)
7563

7664

77-
def _load_fragments(config: list[dict[str, str]]) -> list[Fragment]:
65+
def _load_fragments(config: list[dict[str, str]] | None) -> list[Fragment]:
7866
"""
7967
Load fragments from *config*.
80-
81-
This is a bit more complicated because validating the fragments field using
82-
`oneOf` leads to unhelpful error messages that are difficult to convert
83-
into something humanly meaningful.
84-
85-
So we detect first, validate using jsonschema and try to load them. They
86-
still may fail loading if they refer to files and lack markers / the
87-
pattern doesn't match.
8868
"""
69+
if config is None:
70+
raise ConfigurationError([f"{_BASE}fragments is missing."])
71+
if not config:
72+
raise ConfigurationError([f"{_BASE}fragments must not be empty."])
73+
8974
frags = []
9075
errs = []
9176

92-
for i, frag_cfg in enumerate(config):
77+
for frag_cfg in config:
9378
for frag in VALID_FRAGMENTS:
9479
if frag.key not in frag_cfg:
9580
continue
9681

9782
try:
98-
ves = sorted(
99-
frag.validator.iter_errors(frag_cfg),
100-
key=jsonschema.exceptions.relevance,
101-
)
102-
if ves:
103-
raise ConfigurationError(
104-
errors_to_human_strings(ves, ("fragments", i))
105-
)
10683
frags.append(frag.from_config(frag_cfg))
10784
except ConfigurationError as e:
10885
errs.extend(e.errors)
10986

110-
# We have either detecte and added or detected and errored, but in
87+
# We have either detected and added or detected and errored, but in
11188
# any case we're done with this fragment.
11289
break
11390
else:

src/hatch_fancy_pypi_readme/_fragments.py

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,17 @@
1111
from pathlib import Path
1212
from typing import ClassVar, Iterable
1313

14-
from jsonschema import Validator
15-
1614

1715
if sys.version_info >= (3, 8):
1816
from typing import Protocol
1917
else:
2018
from typing_extensions import Protocol
2119

22-
from ._validators import CustomValidator
2320
from .exceptions import ConfigurationError
2421

2522

26-
TEXT_V = CustomValidator(
27-
{
28-
"$schema": CustomValidator.META_SCHEMA["$id"],
29-
"type": "object",
30-
"properties": {"text": {"type": "string", "minLength": 1}},
31-
"required": ["text"],
32-
"additionalProperties": False,
33-
}
34-
)
35-
36-
FILE_V = CustomValidator(
37-
{
38-
"$schema": CustomValidator.META_SCHEMA["$id"],
39-
"type": "object",
40-
"properties": {
41-
"path": {"type": "string", "minLength": 1},
42-
"start-after": {"type": "string", "minLength": 1},
43-
"end-before": {"type": "string", "minLength": 1},
44-
"pattern": {"type": "string", "regex": True},
45-
},
46-
"required": ["path"],
47-
"additionalProperties": False,
48-
}
49-
)
50-
51-
5223
class Fragment(Protocol):
5324
key: ClassVar[str]
54-
validator: ClassVar[Validator]
5525

5626
@classmethod
5727
def from_config(self, cfg: dict[str, str]) -> Fragment:
@@ -68,13 +38,16 @@ class TextFragment:
6838
"""
6939

7040
key: ClassVar[str] = "text"
71-
validator: ClassVar[Validator] = TEXT_V
7241

7342
_text: str
7443

7544
@classmethod
7645
def from_config(cls, cfg: dict[str, str]) -> Fragment:
77-
return cls(cfg[cls.key])
46+
text = cfg[cls.key]
47+
if not text:
48+
raise ConfigurationError(["Text fragments must not be empty."])
49+
50+
return cls(text)
7851

7952
def render(self) -> str:
8053
return self._text
@@ -87,7 +60,6 @@ class FileFragment:
8760
"""
8861

8962
key: ClassVar[str] = "path"
90-
validator: ClassVar[Validator] = FILE_V
9163

9264
_contents: str
9365

src/hatch_fancy_pypi_readme/_humanize_validation_errors.py

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

src/hatch_fancy_pypi_readme/_substitutions.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import re
88

99
from dataclasses import dataclass
10+
from typing import cast
11+
12+
from hatch_fancy_pypi_readme.exceptions import ConfigurationError
1013

1114

1215
@dataclass
@@ -16,16 +19,37 @@ class Substituter:
1619

1720
@classmethod
1821
def from_config(cls, cfg: dict[str, str]) -> Substituter:
22+
errs = []
1923
flags = 0
2024

2125
ignore_case = cfg.get("ignore-case", False)
26+
if not isinstance(ignore_case, bool):
27+
errs.append(
28+
f"Value {ignore_case!r} for 'ignore-case' is not a bool."
29+
)
30+
2231
if ignore_case:
2332
flags += re.IGNORECASE
2433

25-
pattern = re.compile(cfg["pattern"], flags=flags)
26-
replacement = cfg["replacement"]
27-
28-
return cls(pattern, replacement)
34+
try:
35+
pattern = re.compile(cfg["pattern"], flags=flags)
36+
except KeyError:
37+
errs.append(f"Substitution {cfg} is missing a 'pattern' key.")
38+
except re.error as e:
39+
errs.append(
40+
f"{cfg['pattern']!r} is not a valid regular expression: {e}"
41+
)
42+
43+
replacement = cfg.get("replacement")
44+
if replacement is None:
45+
errs.append(f"Substitution {cfg} is missing a 'replacement' key.")
46+
elif not isinstance(replacement, str):
47+
errs.append(f"Replacement value {replacement!r} is not a string.")
48+
49+
if errs:
50+
raise ConfigurationError(errs)
51+
52+
return cls(pattern, cast(str, replacement))
2953

3054
def substitute(self, text: str) -> str:
3155
return self.pattern.sub(self.replacement, text)

0 commit comments

Comments
 (0)