Skip to content

Commit fe27625

Browse files
authored
Organizing the compatibility layer (#127)
* feat: organize compat directory * feat: refactor compat not in pyodide * feat: restructure _compat module exports * feat: refactor _compat_in_pyodide * fix: resolve mypy
1 parent b838dc7 commit fe27625

8 files changed

Lines changed: 264 additions & 191 deletions

micropip/_compat.py

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

micropip/_compat/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import sys
2+
3+
from .compatibility_layer import CompatibilityLayer
4+
5+
compatibility_layer: type[CompatibilityLayer] | None = None
6+
7+
IN_BROWSER = "_pyodide_core" in sys.modules
8+
9+
if IN_BROWSER:
10+
from ._compat_in_pyodide import CompatibilityInPyodide
11+
12+
compatibility_layer = CompatibilityInPyodide
13+
else:
14+
from ._compat_not_in_pyodide import CompatibilityNotInPyodide
15+
16+
compatibility_layer = CompatibilityNotInPyodide
17+
18+
19+
REPODATA_INFO = compatibility_layer.repodata_info()
20+
21+
REPODATA_PACKAGES = compatibility_layer.repodata_packages()
22+
23+
fetch_bytes = compatibility_layer.fetch_bytes
24+
25+
fetch_string_and_headers = compatibility_layer.fetch_string_and_headers
26+
27+
loadedPackages = compatibility_layer.loadedPackages
28+
29+
loadDynlibsFromPackage = compatibility_layer.loadDynlibsFromPackage
30+
31+
loadPackage = compatibility_layer.loadPackage
32+
33+
get_dynlibs = compatibility_layer.get_dynlibs
34+
35+
to_js = compatibility_layer.to_js
36+
37+
38+
__all__ = [
39+
"REPODATA_INFO",
40+
"REPODATA_PACKAGES",
41+
"fetch_bytes",
42+
"fetch_string_and_headers",
43+
"loadedPackages",
44+
"loadDynlibsFromPackage",
45+
"loadPackage",
46+
"get_dynlibs",
47+
"to_js",
48+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pathlib import Path
2+
from typing import TYPE_CHECKING, Any
3+
from urllib.parse import urlparse
4+
5+
if TYPE_CHECKING:
6+
pass
7+
8+
from pyodide._package_loader import get_dynlibs
9+
from pyodide.ffi import IN_BROWSER, to_js
10+
from pyodide.http import pyfetch
11+
12+
from .compatibility_layer import CompatibilityLayer
13+
14+
try:
15+
import pyodide_js
16+
from pyodide_js import loadedPackages, loadPackage
17+
from pyodide_js._api import ( # type: ignore[import]
18+
loadBinaryFile,
19+
loadDynlibsFromPackage,
20+
)
21+
22+
REPODATA_PACKAGES = pyodide_js._api.repodata_packages.to_py()
23+
REPODATA_INFO = pyodide_js._api.repodata_info.to_py()
24+
except ImportError:
25+
if IN_BROWSER:
26+
raise
27+
# Otherwise, this is pytest test collection so let it go.
28+
29+
30+
class CompatibilityInPyodide(CompatibilityLayer):
31+
@staticmethod
32+
def repodata_info() -> dict[str, str]:
33+
return REPODATA_INFO
34+
35+
@staticmethod
36+
def repodata_packages() -> dict[str, dict[str, Any]]:
37+
return REPODATA_PACKAGES
38+
39+
@staticmethod
40+
async def fetch_bytes(url: str, kwargs: dict[str, str]) -> bytes:
41+
parsed_url = urlparse(url)
42+
if parsed_url.scheme == "emfs":
43+
return Path(parsed_url.path).read_bytes()
44+
if parsed_url.scheme == "file":
45+
return (await loadBinaryFile(parsed_url.path)).to_bytes()
46+
47+
return await (await pyfetch(url, **kwargs)).bytes()
48+
49+
@staticmethod
50+
async def fetch_string_and_headers(
51+
url: str, kwargs: dict[str, str]
52+
) -> tuple[str, dict[str, str]]:
53+
response = await pyfetch(url, **kwargs)
54+
55+
content = await response.string()
56+
headers: dict[str, str] = response.headers
57+
58+
return content, headers
59+
60+
loadedPackages = loadedPackages
61+
62+
get_dynlibs = get_dynlibs
63+
64+
loadDynlibsFromPackage = loadDynlibsFromPackage
65+
66+
loadPackage = loadPackage
67+
68+
to_js = to_js
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import re
2+
from pathlib import Path
3+
from typing import IO, TYPE_CHECKING, Any
4+
from urllib.request import Request, urlopen
5+
from urllib.response import addinfourl
6+
7+
from .compatibility_layer import CompatibilityLayer
8+
9+
if TYPE_CHECKING:
10+
from ..wheelinfo import PackageData
11+
12+
13+
class CompatibilityNotInPyodide(CompatibilityLayer):
14+
15+
# Vendored from packaging
16+
_canonicalize_regex = re.compile(r"[-_.]+")
17+
18+
class loadedPackages(CompatibilityLayer.loadedPackages):
19+
@staticmethod
20+
def to_py():
21+
return {}
22+
23+
@staticmethod
24+
def repodata_info() -> dict[str, str]:
25+
return {}
26+
27+
@staticmethod
28+
def repodata_packages() -> dict[str, dict[str, Any]]:
29+
return {}
30+
31+
@staticmethod
32+
def _fetch(url: str, kwargs: dict[str, Any]) -> addinfourl:
33+
return urlopen(Request(url, **kwargs))
34+
35+
@staticmethod
36+
async def fetch_bytes(url: str, kwargs: dict[str, Any]) -> bytes:
37+
return CompatibilityNotInPyodide._fetch(url, kwargs=kwargs).read()
38+
39+
@staticmethod
40+
async def fetch_string_and_headers(
41+
url: str, kwargs: dict[str, Any]
42+
) -> tuple[str, dict[str, str]]:
43+
response = CompatibilityNotInPyodide._fetch(url, kwargs=kwargs)
44+
headers = {k.lower(): v for k, v in response.headers.items()}
45+
return response.read().decode(), headers
46+
47+
@staticmethod
48+
def get_dynlibs(archive: IO[bytes], suffix: str, target_dir: Path) -> list[str]:
49+
return []
50+
51+
@staticmethod
52+
async def loadDynlibsFromPackage(
53+
pkg_metadata: "PackageData", dynlibs: list[str]
54+
) -> None:
55+
pass
56+
57+
@staticmethod
58+
async def loadPackage(names: str | list[str]) -> None:
59+
pass
60+
61+
@staticmethod
62+
def to_js(
63+
obj: Any,
64+
/,
65+
*,
66+
depth: int = -1,
67+
pyproxies=None,
68+
create_pyproxies: bool = True,
69+
dict_converter=None,
70+
default_converter=None,
71+
) -> Any:
72+
return obj
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from abc import ABC, abstractmethod
2+
from pathlib import Path
3+
from typing import IO, TYPE_CHECKING, Any
4+
5+
if TYPE_CHECKING:
6+
from ..wheelinfo import PackageData
7+
8+
9+
class CompatibilityLayer(ABC):
10+
"""
11+
CompatibilityLayer represents the interface that must be implemented for each viable environment.
12+
13+
All of the following methods / properties must be implemented for use both inside and outside of pyodide.
14+
"""
15+
16+
class loadedPackages(ABC):
17+
@staticmethod
18+
@abstractmethod
19+
def to_py():
20+
pass
21+
22+
@staticmethod
23+
@abstractmethod
24+
def repodata_info() -> dict[str, str]:
25+
pass
26+
27+
@staticmethod
28+
@abstractmethod
29+
def repodata_packages() -> dict[str, dict[str, Any]]:
30+
pass
31+
32+
@staticmethod
33+
@abstractmethod
34+
async def fetch_bytes(url: str, kwargs: dict[str, str]) -> bytes:
35+
pass
36+
37+
@staticmethod
38+
@abstractmethod
39+
async def fetch_string_and_headers(
40+
url: str, kwargs: dict[str, Any]
41+
) -> tuple[str, dict[str, str]]:
42+
pass
43+
44+
@staticmethod
45+
@abstractmethod
46+
def get_dynlibs(archive: IO[bytes], suffix: str, target_dir: Path) -> list[str]:
47+
pass
48+
49+
@staticmethod
50+
@abstractmethod
51+
async def loadDynlibsFromPackage(
52+
pkg_metadata: "PackageData", dynlibs: list[str]
53+
) -> None:
54+
pass
55+
56+
@staticmethod
57+
@abstractmethod
58+
async def loadPackage(names: str | list[str]) -> None:
59+
pass
60+
61+
@staticmethod
62+
@abstractmethod
63+
def to_js(
64+
obj: Any,
65+
/,
66+
*,
67+
depth: int = -1,
68+
pyproxies: Any,
69+
create_pyproxies: bool = True,
70+
dict_converter: Any,
71+
default_converter: Any,
72+
) -> Any:
73+
pass

micropip/_compat_in_pyodide.py

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

0 commit comments

Comments
 (0)