Skip to content

Commit 7f8b376

Browse files
committed
Add loaders.from_traversable for loading from within packages.
1 parent 8312ecf commit 7f8b376

File tree

3 files changed

+96
-16
lines changed

3 files changed

+96
-16
lines changed

noxfile.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def tests(session):
3737
"""
3838
Run the test suite with a corresponding Python version.
3939
"""
40-
session.install("-r", REQUIREMENTS["tests"])
40+
session.install(
41+
"-r",
42+
REQUIREMENTS["tests"],
43+
"importlib-resources; python_version<'3.11'",
44+
)
4145

4246
if session.posargs and session.posargs[0] == "coverage":
4347
if len(session.posargs) > 1 and session.posargs[1] == "github":

referencing_loaders/__init__.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from referencing import Resource, Specification
1212

1313
if TYPE_CHECKING:
14+
from importlib.resources.abc import Traversable
15+
1416
from referencing.typing import URI
1517

1618

@@ -22,21 +24,55 @@ def from_path(root: Path) -> Iterable[tuple[URI, Resource[Any]]]:
2224
the root) -- though it still is often a good idea to explicitly indicate
2325
what specification every resource is written for internally.
2426
"""
25-
specification: Specification[Any] | None = None
26-
for dir, _, files in _walk(root):
27-
for file in files:
28-
path = dir / file
29-
contents = json.loads(path.read_text())
30-
if specification is None:
31-
specification = Specification.detect(contents) # type: ignore[reportUnknownMemberType]
32-
resource = specification.detect(contents).create_resource(contents)
33-
yield path.as_uri(), resource
27+
return _from_walked(_walk(root))
3428

3529

36-
def _walk(path: Path) -> Iterable[tuple[Path, Iterable[str], Iterable[str]]]:
30+
def _walk(path: Path) -> Iterable[Path]:
3731
walk = getattr(path, "walk", None)
38-
if walk is not None:
39-
yield from walk()
40-
return
41-
for root, dirs, files in os.walk(path): # pragma: no cover
42-
yield Path(root), dirs, files
32+
if walk is None:
33+
for dir, _, files in os.walk(path): # pragma: no cover
34+
for file in files:
35+
yield Path(dir) / file
36+
else:
37+
for dir, _, files in walk():
38+
for file in files:
39+
yield dir / file
40+
41+
42+
def _walk_traversable(root: Traversable) -> Iterable[Traversable]:
43+
"""
44+
.walk() for importlib resources paths, which don't have the method :/
45+
""" # noqa: D415
46+
walking = [root]
47+
while walking:
48+
path = walking.pop()
49+
for each in path.iterdir():
50+
if each.is_dir():
51+
walking.append(each)
52+
else:
53+
yield each
54+
55+
56+
def from_traversable(root: Traversable) -> Iterable[tuple[URI, Resource[Any]]]:
57+
"""
58+
Load some resources from a given `importlib.resources` traversable.
59+
60+
(I.e. load schemas from data within a Python package.)
61+
"""
62+
return _from_walked(
63+
each
64+
for each in _walk_traversable(root)
65+
if not each.name.endswith(".py")
66+
)
67+
68+
69+
def _from_walked(
70+
paths: Iterable[Path | Traversable],
71+
) -> Iterable[tuple[URI, Resource[Any]]]:
72+
specification: Specification[Any] | None = None
73+
for path in paths:
74+
contents = json.loads(path.read_text())
75+
if specification is None:
76+
specification = Specification.detect(contents) # type: ignore[reportUnknownMemberType]
77+
resource = specification.detect(contents).create_resource(contents)
78+
yield getattr(path, "as_uri", lambda: "")(), resource

referencing_loaders/tests/test_loaders.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import json
2+
import sys
3+
4+
try:
5+
from importlib.resources import files
6+
except ImportError:
7+
from importlib_resources import files
28

39
from referencing import Registry
410
from referencing.jsonschema import DRAFT202012, EMPTY_REGISTRY
@@ -84,3 +90,37 @@ def test_schema_is_inherited_downwards(tmp_path):
8490
def test_empty(tmp_path):
8591
registry = EMPTY_REGISTRY.with_resources(loaders.from_path(tmp_path))
8692
assert registry == EMPTY_REGISTRY
93+
94+
95+
def test_traversable(tmp_path):
96+
package = tmp_path / "foo"
97+
98+
schemas = package / "schemas"
99+
path, schema = schemas / "schema.json", {
100+
"$schema": "https://json-schema.org/draft/2020-12/schema",
101+
"$id": "http://example.com/",
102+
}
103+
104+
schemas.mkdir(parents=True)
105+
package.joinpath("__init__.py").touch()
106+
path.write_text(json.dumps(schema))
107+
108+
# ?!?! -- without this, importlib.resources.files fails on 3.9 and no other
109+
# version!?!?
110+
schemas.joinpath("__init__.py").touch()
111+
112+
try:
113+
sys.path.append(str(tmp_path))
114+
resources = loaders.from_traversable(files("foo.schemas"))
115+
registry = EMPTY_REGISTRY.with_resources(resources)
116+
117+
assert (
118+
registry.crawl()
119+
== Registry()
120+
.with_contents(
121+
[(path.as_uri(), schema)],
122+
)
123+
.crawl()
124+
)
125+
finally:
126+
sys.path.pop()

0 commit comments

Comments
 (0)