Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,36 @@ Features

- Add `Configurator.resolve_asset` and `Request.resolve_asset` methods. See
https://github.com/Pylons/pyramid/pull/3815.
- Replace all runtime usage of ``pkg_resources`` with :mod:`importlib.resources`.
Pyramid no longer requires ``setuptools`` as a runtime dependency.
- Add :func:`pyramid.path.resource_filename` and :func:`pyramid.path.ref_filename`
helpers for legacy filesystem access to package resources.
- Move :class:`pyramid.resolver.AssetResolver` and
:class:`pyramid.resolver.DottedNameResolver` to the new
:mod:`pyramid.resolver` module (still re-exported from :mod:`pyramid.path`).

Bug Fixes
---------

Backward Incompatibilities
--------------------------

- Asset overrides registered via
:meth:`pyramid.config.Configurator.override_asset` are no longer visible to
third-party code that calls ``pkg_resources`` APIs directly. Overrides are
now applied explicitly by Pyramid's own asset resolution APIs (such as
:class:`pyramid.resolver.AssetResolver`).
- :class:`pyramid.interfaces.IPackageOverrides` no longer extends
:class:`pyramid.interfaces.IPEP302Loader`. The ``OverrideProvider`` class
and ``register_loader_type`` integration have been removed.
- :class:`pyramid.config.assets.PackageOverrides` no longer sets
``package.__loader__``.

Deprecations
------------

Documentation Changes
---------------------

- Update asset resolution documentation to describe ``importlib.resources``
instead of ``pkg_resources``.
4 changes: 3 additions & 1 deletion CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,6 @@ Contributors

- Camill Kaipf, 2022/08/12

- Stefano Rivera, 2022/12/29
- Stefano Rivera, 2022/12/29

- Rémy Hubscher, 2026/08/05
7 changes: 3 additions & 4 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ Glossary
distribution, and installation of libraries and applications.

pkg_resources
A module which ships with :term:`Setuptools` that
A deprecated module which ships with :term:`Setuptools` that
provides an API for addressing "asset files" within a Python
:term:`package`. Asset files are static files, template files, etc;
basically anything non-Python-source that lives in a Python package can
be considered a asset file.
:term:`package`. :app:`Pyramid` no longer uses ``pkg_resources``;
asset resolution is performed using :mod:`importlib.resources` instead.

.. seealso::

Expand Down
26 changes: 14 additions & 12 deletions docs/narr/assets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ two parts:

The two parts are separated by a colon ``:`` character.

:app:`Pyramid` uses the Python :term:`pkg_resources` API to resolve the package
name and asset name to an absolute (operating system-specific) file name. It
eventually passes this resolved absolute filesystem path to the Chameleon
templating engine, which then uses it to load, parse, and execute the template
file.
:app:`Pyramid` uses the Python :mod:`importlib.resources` API to resolve the
package name and asset name to an absolute (operating system-specific) file
name. It eventually passes this resolved absolute filesystem path to the
Chameleon templating engine, which then uses it to load, parse, and execute
the template file.

There is a second form of asset specification: a *relative* asset
specification. Instead of using an "absolute" asset specification which
Expand Down Expand Up @@ -801,8 +801,10 @@ feature, a :term:`Configurator` API exists named
- A directory of static files served up by an instance of the
``pyramid.static.static_view`` helper class.

- Any other asset (or set of assets) addressed by code that uses the Setuptools
:term:`pkg_resources` API.
- Any other asset (or set of assets) addressed by code that uses Pyramid's
asset resolution APIs (such as
:class:`pyramid.path.AssetResolver` and
:func:`pyramid.path.resource_filename`).

.. index::
single: override_asset
Expand Down Expand Up @@ -875,11 +877,11 @@ path. The first asset that exists in the search path will be used; if no asset
exists in the override path, the original asset is used.

Asset overrides can actually override assets other than templates and static
files. Any software which uses the
:func:`pkg_resources.get_resource_filename`,
:func:`pkg_resources.get_resource_stream`, or
:func:`pkg_resources.get_resource_string` APIs will obtain an overridden file
when an override is used.
files. Any code that resolves assets through Pyramid's asset APIs (such as
:meth:`pyramid.path.AssetResolver.resolve`,
:func:`pyramid.path.resource_filename`, or
:func:`pyramid.asset.abspath_from_asset_spec`) will obtain an overridden file
when an override is registered.

.. versionadded:: 1.6
As of Pyramid 1.6, it is also possible to override an asset by supplying an
Expand Down
5 changes: 2 additions & 3 deletions docs/narr/environment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,9 +456,8 @@ the distinction can be confusing. It's helpful to read
When ``pyramid.reload_templates`` is true, :app:`Pyramid` takes advantage of
the underlying templating system's ability to check for file modifications to
an individual template file. When ``pyramid.reload_templates`` is true, but
``pyramid.reload_assets`` is *not* true, the template filename returned by the
``pkg_resources`` package (used under the hood by asset resolution) is cached
by :app:`Pyramid` on the first request. Subsequent requests for the same
``pyramid.reload_assets`` is *not* true, the template filename returned by
asset resolution is cached by :app:`Pyramid` on the first request. Subsequent requests for the same
template file will return a cached template filename. The underlying
templating system checks for modifications to this particular file for every
request. Setting ``pyramid.reload_templates`` to ``True`` doesn't affect
Expand Down
4 changes: 2 additions & 2 deletions docs/narr/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ The fundamental "plug points" of an application developed using :app:`Pyramid`
are *routes*, *views*, and *assets*. Routes are declarations made using the
:meth:`pyramid.config.Configurator.add_route` method. Views are declarations
made using the :meth:`pyramid.config.Configurator.add_view` method. Assets are
files that are accessed by :app:`Pyramid` using the :term:`pkg_resources` API
such as static files and templates via a :term:`asset specification`. Other
files that are accessed by :app:`Pyramid` using the :mod:`importlib.resources`
API such as static files and templates via a :term:`asset specification`. Other
directives and configurator methods also deal in routes, views, and assets.
For example, the ``add_handler`` directive of the ``pyramid_handlers`` package
adds a single route and some number of views.
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ dependencies = [
"hupper >= 1.5", # ignore_files support
"plaster",
"plaster_pastedeploy",
"setuptools < 82", # require pkg_resources
"translationstring >= 0.4", # py3 compat
"venusian >= 1.0", # ``ignore``
"webob >= 1.8.3", # Accept.parse_offer
Expand Down
4 changes: 2 additions & 2 deletions src/pyramid/asset.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import pkg_resources

from pyramid.path import package_name, package_path
from pyramid.resolver import AssetResolver


def resolve_asset_spec(spec, pname='__main__'):
Expand Down Expand Up @@ -40,4 +40,4 @@ def abspath_from_asset_spec(spec, pname='__main__'):
pname, filename = resolve_asset_spec(spec, pname)
if pname is None:
return filename
return pkg_resources.resource_filename(pname, filename)
return AssetResolver(None).resolve(f'{pname}:{filename}').abspath()
12 changes: 5 additions & 7 deletions src/pyramid/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,9 @@
IDebugLogger,
IExceptionResponse,
)
from pyramid.path import (
AssetResolver,
DottedNameResolver,
caller_package,
package_of,
)
from pyramid.path import caller_package, package_of
from pyramid.registry import Introspectable, Introspector, Registry
from pyramid.resolver import AssetResolver, DottedNameResolver
from pyramid.router import Router
from pyramid.settings import aslist
from pyramid.threadlocal import manager
Expand Down Expand Up @@ -297,7 +293,6 @@ def __init__(
self.name_resolver = name_resolver
self.package_name = name_resolver.get_package_name()
self.package = name_resolver.get_package()
self.asset_resolver = AssetResolver(self.package)
self.root_package = root_package
self.registry = registry
self.autocommit = autocommit
Expand All @@ -322,6 +317,9 @@ def __init__(
default_view_mapper=default_view_mapper,
exceptionresponse_view=exceptionresponse_view,
)
self.asset_resolver = AssetResolver(
self.package, registry=self.registry
)

def setup_registry(
self,
Expand Down
166 changes: 28 additions & 138 deletions src/pyramid/config/assets.py
Original file line number Diff line number Diff line change
@@ -1,110 +1,17 @@
import importlib.resources
import os
import pkg_resources
import sys
from zope.interface import implementer

from pyramid.config.actions import action_method
from pyramid.exceptions import ConfigurationError
from pyramid.interfaces import PHASE1_CONFIG, IPackageOverrides
from pyramid.threadlocal import get_current_registry


class OverrideProvider(pkg_resources.DefaultProvider):
def __init__(self, module):
pkg_resources.DefaultProvider.__init__(self, module)
self.module_name = module.__name__

def _get_overrides(self):
reg = get_current_registry()
overrides = reg.queryUtility(IPackageOverrides, self.module_name)
return overrides

def get_resource_filename(self, manager, resource_name):
"""Return a true filesystem path for resource_name,
co-ordinating the extraction with manager, if the resource
must be unpacked to the filesystem.
"""
overrides = self._get_overrides()
if overrides is not None:
filename = overrides.get_filename(resource_name)
if filename is not None:
return filename
return pkg_resources.DefaultProvider.get_resource_filename(
self, manager, resource_name
)

def get_resource_stream(self, manager, resource_name):
"""Return a readable file-like object for resource_name."""
overrides = self._get_overrides()
if overrides is not None:
stream = overrides.get_stream(resource_name)
if stream is not None:
return stream
return pkg_resources.DefaultProvider.get_resource_stream(
self, manager, resource_name
)

def get_resource_string(self, manager, resource_name):
"""Return a string containing the contents of resource_name."""
overrides = self._get_overrides()
if overrides is not None:
string = overrides.get_string(resource_name)
if string is not None:
return string
return pkg_resources.DefaultProvider.get_resource_string(
self, manager, resource_name
)

def has_resource(self, resource_name):
overrides = self._get_overrides()
if overrides is not None:
result = overrides.has_resource(resource_name)
if result is not None:
return result
return pkg_resources.DefaultProvider.has_resource(self, resource_name)

def resource_isdir(self, resource_name):
overrides = self._get_overrides()
if overrides is not None:
result = overrides.isdir(resource_name)
if result is not None:
return result
return pkg_resources.DefaultProvider.resource_isdir(
self, resource_name
)

def resource_listdir(self, resource_name):
overrides = self._get_overrides()
if overrides is not None:
result = overrides.listdir(resource_name)
if result is not None:
return result
return pkg_resources.DefaultProvider.resource_listdir(
self, resource_name
)
from pyramid.path import ref_filename


@implementer(IPackageOverrides)
class PackageOverrides:
# pkg_resources arg in kw args below for testing
def __init__(self, package, pkg_resources=pkg_resources):
loader = self._real_loader = getattr(package, '__loader__', None)
if isinstance(loader, self.__class__):
self._real_loader = None
# We register ourselves as a __loader__ *only* to support the
# setuptools _find_adapter adapter lookup; this class doesn't
# actually support the PEP 302 loader "API". This is
# excusable due to the following statement in the spec:
# ... Loader objects are not
# required to offer any useful functionality (any such functionality,
# such as the zipimport get_data() method mentioned above, is
# optional)...
# A __loader__ attribute is basically metadata, and setuptools
# uses it as such.
package.__loader__ = self
# we call register_loader_type for every instantiation of this
# class; that's OK, it's idempotent to do it more than once.
pkg_resources.register_loader_type(self.__class__, OverrideProvider)
def __init__(self, package):
self.overrides = []
self.overridden_package_name = package.__name__

Expand Down Expand Up @@ -163,28 +70,6 @@ def listdir(self, resource_name):
if result is not None:
return result

@property
def real_loader(self):
if self._real_loader is None:
raise NotImplementedError()
return self._real_loader

def get_data(self, path):
"""See IPEP302Loader."""
return self.real_loader.get_data(path)

def is_package(self, fullname):
"""See IPEP302Loader."""
return self.real_loader.is_package(fullname)

def get_code(self, fullname):
"""See IPEP302Loader."""
return self.real_loader.get_code(fullname)

def get_source(self, fullname):
"""See IPEP302Loader."""
return self.real_loader.get_source(fullname)


class DirectoryOverride:
def __init__(self, path, source):
Expand Down Expand Up @@ -229,40 +114,45 @@ def __init__(self, package, prefix):
def get_path(self, resource_name):
return f'{self.prefix}{resource_name}'

def get_spec(self, resource_name):
def _ref(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return f'{self.pkg_name}:{path}'
return importlib.resources.files(self.pkg_name) / path

def get_spec(self, resource_name):
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return f'{self.pkg_name}:{self.get_path(resource_name)}'

def get_filename(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return pkg_resources.resource_filename(self.pkg_name, path)
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return ref_filename(ref)

def get_stream(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return pkg_resources.resource_stream(self.pkg_name, path)
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return ref.open('rb')

def get_string(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return pkg_resources.resource_string(self.pkg_name, path)
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
with ref.open('rb') as fh:
return fh.read()

def exists(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return True

def isdir(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return pkg_resources.resource_isdir(self.pkg_name, path)
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return ref.is_dir()

def listdir(self, resource_name):
path = self.get_path(resource_name)
if pkg_resources.resource_exists(self.pkg_name, path):
return pkg_resources.resource_listdir(self.pkg_name, path)
ref = self._ref(resource_name)
if ref.is_file() or ref.is_dir():
return [x.name for x in ref.iterdir()]


class FSAssetSource:
Expand Down
Loading