File: advanced_topics.rst

package info (click to toggle)
python-phx-class-registry 4.1.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 260 kB
  • sloc: python: 886; makefile: 19
file content (196 lines) | stat: -rw-r--r-- 5,680 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
Advanced Topics
===============
This section covers more advanced or esoteric uses of ClassRegistry features.

Registering Classes Automatically
---------------------------------
Tired of having to add the ``register`` decorator to every class that you want
to add to a class registry?  Surely there's a better way!

ClassRegistry also provides an :py:func:`AutoRegister` metaclass that you can
apply to a base class.  Any non-abstract subclass that extends that base class
will be registered automatically.

Here's an example:

.. code-block:: python

   from abc import abstractmethod
   from class_registry import AutoRegister, ClassRegistry

   pokedex = ClassRegistry('element')

   # Note ``AutoRegister(pokedex)`` used as the metaclass here.
   class Pokemon(metaclass=AutoRegister(pokedex)):
      @abstractmethod
      def get_abilities(self):
        raise NotImplementedError()

   # Define some non-abstract subclasses.
   class Butterfree(Pokemon):
     element = 'bug'

     def get_abilities(self):
       return ['compound_eyes']

   class Spearow(Pokemon):
     element = 'flying'

     def get_abilities(self):
       return ['keen_eye']

   # Any non-abstract class that extends ``Pokemon`` will automatically
   # get registered in our Pokédex!
   assert list(pokedex.items()) == \
     [('bug', Butterfree), ('flying', Spearow)]

In the above example, note that ``Butterfree`` and ``Spearow`` were added to
``pokedex`` automatically.  However, the ``Pokemon`` base class was not added,
because it is abstract.

.. important::

   Python defines an abstract class as a class with at least one unimplemented
   abstract method.  You can't just add ``metaclass=ABCMeta``!

   .. code-block:: python

      from abc import ABCMeta

      # Declare an "abstract" class.
      class ElectricPokemon(Pokemon, metaclass=ABCMeta):
        element = 'electric'

        def get_abilities(self):
          return ['shock']

      assert list(pokedex.items()) == \
        [('bug', Butterfree), \
         ('flying', Spearow), \
         ('electric', ElectricPokemon)]

   Note in the above example that ``ElectricPokemon`` was added to ``pokedex``,
   even though its metaclass is :py:class:`ABCMeta`.

   Because ``ElectricPokemon`` doesn't have any unimplemented abstract methods,
   Python does **not** consider it to be abstract.

   We can verify this by using :py:func:`inspect.isabstract`:

   .. code-block:: python

      from inspect import isabstract
      assert not isabstract(ElectricPokemon)


Patching
--------
From time to time, you might need to register classes temporarily.  For example,
you might need to patch a global class registry in a unit test, ensuring that
the extra classes are removed when the test finishes.

ClassRegistry provides a :py:class:`RegistryPatcher` that you can use for just
such a purpose:

.. code-block:: python

   from class_registry import ClassRegistry, RegistryKeyError, \
     RegistryPatcher

   pokedex = ClassRegistry('element')

   # Create a couple of new classes, but don't register them yet!
   class Oddish(object):
     element = 'grass'

   class Meowth(object):
     element = 'normal'

   # As expected, neither of these classes are registered.
   try:
     pokedex['grass']
   except RegistryKeyError:
     pass

   # Use a patcher to temporarily register these classes.
   with RegistryPatcher(pokedex, Oddish, Meowth):
     abbot = pokedex['grass']
     assert isinstance(abbot, Oddish)

     costello = pokedex['normal']
     assert isinstance(costello, Meowth)

   # Outside the context, the classes are no longer registered!
   try:
     pokedex['grass']
   except RegistryKeyError:
     pass

If desired, you can also change existing registry keys, or even replace a class
that is already registered.

.. code-block:: python

   @pokedex.register
   class Squirtle(object):
     element = 'water'

   # Get your diving suit Meowth; we're going to Atlantis!
   with RegistryPatcher(pokedex, water=Meowth):
     nemo = pokedex['water']
     assert isinstance(nemo, Meowth)

   # After the context exits, the previously-registered class is
   # restored.
   ponsonby = pokedex['water']
   assert isinstance(ponsonby, Squirtle)

.. important::

   Only mutable registries can be patched (any class that extends
   :py:class:`BaseMutableRegistry`).

   In particular, this means that :py:class:`EntryPointClassRegistry` can
   **not** be patched using :py:class:`RegistryPatcher`.


Overriding Lookup Keys
----------------------
In some cases, you may want to customise the way a ``ClassRegistry`` looks up
which class to use.  For example, you may need to change the registry key for a
particular class, but you want to maintain backwards-compatibility for existing
code that references the old key.

To customise this, create a subclass of ``ClassRegistry`` and override its
``gen_lookup_key`` method:

.. code-block:: python

   class FacadeRegistry(ClassRegistry):
     @staticmethod
     def gen_lookup_key(key: str) -> str:
         """
         In a previous version of the codebase, some pokémon had the 'bird'
         type, but this was later dropped in favour of 'flying'.
         """
         if key == 'bird':
             return 'flying'

         return key

   pokedex = FacadeRegistry('element')

   @pokedex.register
   class MissingNo(Pokemon):
       element = 'flying'

   @pokedex.register
   class Meowth(object):
     element = 'normal'

   # MissingNo can be accessed by either key.
   assert isinstance(pokedex['bird'], MissingNo)
   assert isinstance(pokedex['flying'], MissingNo)

   # Other pokémon work as you'd expect.
   assert isinstance(pokedex['normal'], Meowth)