Skip to content

Commit 4d87d9f

Browse files
Bundle databackend
1 parent cce839f commit 4d87d9f

File tree

6 files changed

+166
-0
lines changed

6 files changed

+166
-0
lines changed

pins/_databackend.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""See https://github.com/machow/databackend"""
2+
3+
# MIT License
4+
# Copyright (c) 2024 databackend contributors
5+
# See pins/tests/_databackend/LICENSE
6+
# commit 214832b, on 31/07/2024, v0.0.2+dev
7+
8+
import importlib
9+
import sys
10+
from abc import ABCMeta
11+
12+
13+
def _load_class(mod_name: str, cls_name: str):
14+
mod = importlib.import_module(mod_name)
15+
return getattr(mod, cls_name)
16+
17+
18+
class _AbstractBackendMeta(ABCMeta):
19+
def register_backend(cls, mod_name: str, cls_name: str):
20+
cls._backends.append((mod_name, cls_name))
21+
cls._abc_caches_clear()
22+
23+
24+
class AbstractBackend(metaclass=_AbstractBackendMeta):
25+
@classmethod
26+
def __init_subclass__(cls):
27+
if not hasattr(cls, "_backends"):
28+
cls._backends = []
29+
30+
@classmethod
31+
def __subclasshook__(cls, subclass):
32+
for mod_name, cls_name in cls._backends:
33+
if mod_name not in sys.modules:
34+
# module isn't loaded, so it can't be the subclass
35+
# we don't want to import the module to explicitly run the check
36+
# so skip here.
37+
continue
38+
else:
39+
parent_candidate = _load_class(mod_name, cls_name)
40+
if issubclass(subclass, parent_candidate):
41+
return True
42+
43+
return NotImplemented

pins/tests/_databackend/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 databackend contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

pins/tests/_databackend/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class ADataClass:
2+
pass
3+
4+
5+
class ADataClass2:
6+
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class UnimportedClass:
2+
pass
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""See https://github.com/machow/databackend"""
2+
3+
import importlib
4+
import sys
5+
6+
import pytest
7+
8+
from pins._databackend import AbstractBackend
9+
from pins.tests._databackend.a_data_class import ADataClass, ADataClass2
10+
11+
CLASS_MOD = "pins.tests._databackend.a_data_class"
12+
CLASS_NAME = "ADataClass"
13+
14+
15+
@pytest.fixture
16+
def Base():
17+
class Base(AbstractBackend):
18+
pass
19+
20+
Base.register_backend(CLASS_MOD, CLASS_NAME)
21+
22+
return Base
23+
24+
25+
def test_check_unimported_mod(Base):
26+
class ABase(AbstractBackend):
27+
pass
28+
29+
mod_name = "pins.tests._databackend.an_unimported_module"
30+
ABase.register_backend(mod_name, "UnimportedClass")
31+
32+
# check pre-import and verify it's still not imported ----
33+
assert not issubclass(int, ABase)
34+
assert mod_name not in sys.modules
35+
36+
# do import and verify ABC is seen as parent class ----
37+
mod = importlib.import_module(mod_name)
38+
39+
assert issubclass(mod.UnimportedClass, ABase)
40+
41+
42+
def test_issubclass(Base):
43+
assert issubclass(ADataClass, Base)
44+
45+
46+
def test_isinstance(Base):
47+
assert isinstance(ADataClass(), Base)
48+
49+
50+
def test_check_is_cached():
51+
checks = [0]
52+
53+
class ABase(AbstractBackend):
54+
@classmethod
55+
def __subclasshook__(cls, subclass):
56+
# increment the number in checks, as a dumb way
57+
# of seeing how often this runs
58+
# could also use abc.ABCMeta._dump_registry
59+
checks[0] = checks[0] + 1
60+
return super().__subclasshook__(subclass)
61+
62+
# this check runs subclasshook ----
63+
issubclass(ADataClass, ABase)
64+
assert checks[0] == 1
65+
66+
# now that ADataClass is in the abc.ABCMeta cache, it
67+
# does *not* run subclasshook
68+
issubclass(ADataClass, ABase)
69+
assert checks[0] == 1
70+
71+
72+
def test_backends_spec_at_class_declaration():
73+
class ABase(AbstractBackend):
74+
_backends = [(CLASS_MOD, CLASS_NAME)]
75+
76+
assert issubclass(ADataClass, ABase)
77+
78+
79+
def test_backends_do_not_overlap():
80+
class ABase1(AbstractBackend): ...
81+
82+
class ABase2(AbstractBackend): ...
83+
84+
ABase1.register_backend(CLASS_MOD, "ADataClass")
85+
ABase2.register_backend(CLASS_MOD, "ADataClass2")
86+
87+
obj1 = ADataClass()
88+
obj2 = ADataClass2()
89+
90+
assert isinstance(obj1, ABase1)
91+
assert not isinstance(obj1, ABase2)
92+
93+
assert not isinstance(obj2, ABase1)
94+
assert isinstance(obj2, ABase2)

0 commit comments

Comments
 (0)