fictive.patterns.package_override module¶
helper functions for overriding package shared resources
Frameworks, related packages, and other interoperating libraries and modules will often rely on some shared resource(s). E.g.:
''' some_package.py '''
class Base(object):
''' shared attributes '''
class Widget(Base):
''' widget attributes'''
class Sprocket(Base):
''' sprocket attributes'''
If another project wants to modify the shared resource (in the above
case, by subclassing some_package.Base), it can be difficult to
propogate those changes to the rest of the framework’s resources. For
example, because some_package.Widget and some_package.Sprocket are
a subclasss of some_package.Base, creating a subclass
my_project.MyBase will have no effect on some_package.Widget or
some_package.Sprocekt. Obtaining Widget and Sprocket classes
that incorporate my_project.Base would require also subclassing
Widget and Sprocket. I.e.:
''' my_project.py '''
from some_package import Base, Widget, Sprocket
class MyBase(Base):
''' custom shared attributes '''
class MyWidget(Widget):
pass
class MySprocket(Sprocket):
pass
Furthermore, any additional references to the orgiinal resources might also need to be overriden. E.g.:
''' my_project.py (continued) '''
...
class PartFactory(object):
part_catalog = {'widget': Widget, 'sprocket': Sprocket}
def make_part(self, name):
''' creates a new part based on the order text '''
return self.part_catalog[name]()
Here, PartFactory would return instances of the original classes
(i.e., base.Widget and base.Sprocket), so the project would also
likely need to subclass PartFactory. For large packages, this type
of ad hoc subclassing can quickly become intractible.
An alternative solution is to use “moneky patch” some_package.Base, i.e.,
dynamically change the properties of some_package.Base while the
project’s code is executing. E.g.:
''' my_project.py '''
from some_package import Base
def new_method(self):
'''' do some common thing '''
Base.new_method = new_method
Unfortunately, monkey patching can often fail to addesss more complicated behaviors and interdependencies within the package. Furthermore, the result can be sensitive to the order in which modules are imported and loaded, which can create situations that are difficult to diagnose and debug. While some situations call for monkey patching, it’s an inelegant, fragile, and unidiomatic solution.
On the other hand, if a package developer foresees some package resources as likely targets for customization, the package can be structured to facilitate that customization.
This module provides means for exposing a package resource to being overridden using and environment variable, a configuration file, or other installed package. The override can occur when the package resource is initially imported and so will automatically propogate to any other pakcage resources that use the overridden resource. E.g.:
''' some_package/base.py '''
class DefaultBase(object):
''' shared attributes '''
''' some_package/parts.py '''
from fictive.patterns.package_override import package_override
#: will check the default file location (entry_points.txt) and
#: the working set for a 'Base' entry point in the
#: 'some_package.base.overrides' group; if no such entry point is
#: found, use the package default
Base = package_override(
object_reference='some_package.base:DefaultBase',
group='some_package.base.overrides', name='Base')
class Widget(Base):
''' widget attributes'''
class Sprocket(Base):
''' sprocket attributes'''
class PartFactory(object):
part_catalog = {'widget': Widget, 'sprocket': Sprocket}
def make_part(self, name):
''' creates a new part based on the order text '''
return self.part_catalog[name]()
''' my_project/base.py '''
from some_package.base import DefaultBase
class CustomBase(DefaultBase):
pass
# entry_points.txt
[some_package.base.overrides]
Base = my_project.base:CustomBase
'''' my_project/app.py '''
from some_package.parts import Widget, Sprocket, PartFactory
from my_project.base import CustomBase
>>> issubclass(Widget, CustomBase)
True
>>> isinstance(Sprocket(), CustomBase)
True
>>> isinstance(PartFactory().make_part('sprocket'), CustomBase)
True
Functions
dynamically load resource from the specified source(s) |
-
fictive.patterns.package_override.package_override(object_reference: str = None, env_key: str = None, filename: str = 'entry_points.txt', working_set: pkg_resources.WorkingSet = None, group: str = None, name: str = None, multiple: Literal[raise, warn, silent, iter] = 'raise')[source]¶ dynamically load resource from the specified source(s)
- Parameters
object_reference (str) – an object reference (see https://packaging.python.org/specifications/entry-points/#data-model) indicating the default object to load if no other overrides are found
env_key (str) – an environment key to check for an object reference (see https://packaging.python.org/specifications/entry-points/#data-model). If the key is set, will attempt to
pkg_resources.EntryPoint.resolve()the valuefilename (str) – the file name containing an entry points file (see https://packaging.python.org/specifications/entry-points/#file-format).
working_set (pkg_resources.WorkingSet) – teh working set to search for the specified entry point
group (str) – the entry point group to search for an entry point record to resolve. REQUIRED if loading from a file or working set
name (str) – the name of the entry point to resolve
multiple –
pkg_resourcespermits a working set to have multiple entry points with the same group and name. In the event that multiple entry points match thegroupandnamevalues provided, the value of themultipleparameter controls the result:- ’raise’ (default):
raises a
pkg_resources.ResolutionError
- ’warn’:
a warning is issued and one of the matching entry points is arbitrarily chosen and loaded
- ’silent’:
one of the matching entry points is arbitrarily chosen and loaded without any warning
- Raises
ValueError – the object reference contains one or more syntax errors
pkg_resources.ResolutionError – there are multiple entry points in the specified group with the specified name and
multipleis'raise'ImportError – the entry point could not be loaded