fix: vendor minimal pkg_resources shim so pyramid survives setuptools>=81

setuptools 81 removed pkg_resources from its distribution. pyramid 2.0.x
(and 2.1) still does `import pkg_resources`. When setuptools 82.0.1 landed
in /opt/remarkbox/env, pyramid failed to import, all three uwsgi services
on origin crash-looped, and my.remarkbox.com / meta.remarkbox.com /
foxhop.net / westworld2.com served 502.

remarkbox/_vendor/pkg_resources/__init__.py is a 115-line shim backed by
importlib.resources (stdlib only — no setuptools, no jaraco.text, no
platformdirs). Exposes exactly the surface pyramid uses:
resource_filename / resource_stream / resource_string / resource_exists /
resource_isdir / resource_listdir / DefaultProvider / register_loader_type.

remarkbox/__init__.py prepends our _vendor dir to sys.path before the
first pyramid import so `import pkg_resources` always finds our shim,
regardless of which setuptools is installed. Bleeding-edge friendly:
upstream setuptools removals can no longer break us.

558 tests pass.
This commit is contained in:
russell@unturf.com 2026-05-24 10:44:05 -04:00
parent 05f51ff965
commit 112d3fd3b1
No known key found for this signature in database
3 changed files with 125 additions and 0 deletions

View file

@ -1,3 +1,13 @@
# Vendored pkg_resources shim: setuptools 81 dropped pkg_resources, but pyramid
# still imports it. Inject our _vendor dir into sys.path BEFORE the pyramid
# import below so `import pkg_resources` finds our shim. See
# remarkbox/_vendor/pkg_resources/__init__.py.
import os as _rb_os
import sys as _rb_sys
_rb_vendor = _rb_os.path.join(_rb_os.path.dirname(__file__), "_vendor")
if _rb_vendor not in _rb_sys.path:
_rb_sys.path.insert(0, _rb_vendor)
from pyramid.config import Configurator
from sqlalchemy import engine_from_config

View file

View file

@ -0,0 +1,115 @@
"""
Minimal vendored pkg_resources shim for Remarkbox.
setuptools 81 dropped pkg_resources from its distribution. Pyramid (and a
handful of other libraries) still import it. This shim provides the narrow
surface pyramid actually uses, backed entirely by stdlib (importlib.resources,
importlib.import_module). No setuptools coupling, no jaraco.text, no
platformdirs. Bleeding-edge friendly.
Surface (only what pyramid touches):
resource_filename(package, name) -> str
resource_stream(package, name) -> IO[bytes]
resource_string(package, name) -> bytes
resource_exists(package, name) -> bool
resource_isdir(package, name) -> bool
resource_listdir(package, name) -> list[str]
DefaultProvider -- class, subclassable
register_loader_type -- no-op registry stub
If a future dependency needs more of the legacy pkg_resources API, extend
this file. Do not pull in upstream setuptools' pkg_resources/__init__.py
(3700 lines + jaraco.text + platformdirs); that defeats the point.
"""
import os
from importlib.resources import files
def _ref(package, name=""):
if not isinstance(package, str):
package = package.__name__
ref = files(package)
if name:
ref = ref / name
return ref
def resource_filename(package, name):
return str(_ref(package, name))
def resource_stream(package, name):
return _ref(package, name).open("rb")
def resource_string(package, name):
return _ref(package, name).read_bytes()
def resource_exists(package, name):
try:
ref = _ref(package, name)
except (FileNotFoundError, ModuleNotFoundError):
return False
try:
return ref.is_file() or ref.is_dir()
except (FileNotFoundError, NotADirectoryError):
return False
def resource_isdir(package, name):
try:
return _ref(package, name).is_dir()
except (FileNotFoundError, ModuleNotFoundError, NotADirectoryError):
return False
def resource_listdir(package, name):
try:
return [child.name for child in _ref(package, name).iterdir()]
except (FileNotFoundError, ModuleNotFoundError, NotADirectoryError):
return []
class DefaultProvider:
"""Minimal stand-in for pkg_resources.DefaultProvider.
Pyramid subclasses this to wire its asset-override system. The `manager`
arg in get_resource_* (originally a ResourceManager for zip-egg cache
extraction) is unused modern pip installs are unpacked directories.
"""
def __init__(self, module):
self.module = module
self.module_path = None
if getattr(module, "__file__", None):
self.module_path = os.path.dirname(module.__file__)
def _name(self):
return self.module.__name__
def get_resource_filename(self, manager, resource_name):
return resource_filename(self._name(), resource_name)
def get_resource_stream(self, manager, resource_name):
return resource_stream(self._name(), resource_name)
def get_resource_string(self, manager, resource_name):
return resource_string(self._name(), resource_name)
def has_resource(self, resource_name):
return resource_exists(self._name(), resource_name)
def resource_isdir(self, resource_name):
return resource_isdir(self._name(), resource_name)
def resource_listdir(self, resource_name):
return resource_listdir(self._name(), resource_name)
_LOADER_TYPES = {}
def register_loader_type(loader_class, provider_class):
_LOADER_TYPES[loader_class] = provider_class