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

package_override

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 value

  • filename (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_resources permits a working set to have multiple entry points with the same group and name. In the event that multiple entry points match the group and name values provided, the value of the multiple parameter 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 multiple is 'raise'

  • ImportError – the entry point could not be loaded