class-registry-4.1.0/0000755000175000017510000000000014507661043014024 5ustar nileshnileshclass-registry-4.1.0/tox.ini0000644000175000017510000000010714507661043015335 0ustar nileshnilesh[tox] envlist = py3{12,11,10} [testenv] commands = python -m unittest class-registry-4.1.0/test/0000755000175000017510000000000014507661043015003 5ustar nileshnileshclass-registry-4.1.0/test/test_registry.py0000644000175000017510000003325614507661043020275 0ustar nileshnileshfrom functools import cmp_to_key from unittest import TestCase from class_registry.registry import ClassRegistry, RegistryKeyError, \ SortedClassRegistry from test import Bulbasaur, Charmander, Charmeleon, Pokemon, \ Squirtle, Wartortle class ClassRegistryTestCase(TestCase): def test_register_manual_keys(self): """ Registers a few classes with manually-assigned identifiers and verifies that the factory returns them correctly. """ registry = ClassRegistry() @registry.register('fire') class Charizard(Pokemon): pass @registry.register('water') class Blastoise(Pokemon): pass # By default, you have to specify a registry key when registering new # classes. We'll see how to assign registry keys automatically in the # next test. with self.assertRaises(ValueError): @registry.register class Venusaur(Pokemon): pass self.assertIsInstance(registry['fire'], Charizard) self.assertIsInstance(registry['water'], Blastoise) def test_register_detect_keys(self): """ If an attribute name is passed to ClassRegistry's constructor, it will automatically check for this attribute when registering classes. """ registry = ClassRegistry(attr_name='element') @registry.register class Charizard(Pokemon): element = 'fire' @registry.register class Blastoise(Pokemon): element = 'water' # You can still override the registry key if you want. @registry.register('poison') class Venusaur(Pokemon): element = 'grass' self.assertIsInstance(registry['fire'], Charizard) self.assertIsInstance(registry['water'], Blastoise) self.assertIsInstance(registry['poison'], Venusaur) # We overrode the registry key for this class. with self.assertRaises(RegistryKeyError): # noinspection PyStatementEffect registry['grass'] def test_register_error_empty_key(self): """ Attempting to register a class with an empty key. """ registry = ClassRegistry('element') with self.assertRaises(ValueError): # noinspection PyTypeChecker @registry.register(None) class Ponyta(Pokemon): element = 'fire' with self.assertRaises(ValueError): @registry.register('') class Rapidash(Pokemon): element = 'fire' with self.assertRaises(ValueError): @registry.register class Mew(Pokemon): element = None with self.assertRaises(ValueError): @registry.register class Mewtwo(Pokemon): element = '' def test_unique_keys(self): """ Specifying ``unique=True`` when creating the registry requires all keys to be unique. """ registry = ClassRegistry(attr_name='element', unique=True) # We can register any class like normal... registry.register(Charmander) # ... but if we try to register a second class with the same key, we # get an error. with self.assertRaises(RegistryKeyError): registry.register(Charmeleon) def test_unregister(self): """ Removing a class from the registry. .. note:: This is not used that often outside unit tests (e.g., to remove artefacts when a test has to add a class to a global registry). """ registry = ClassRegistry(attr_name='element') registry.register(Charmander) registry.register(Squirtle) self.assertIs(registry.unregister('fire'), Charmander) with self.assertRaises(RegistryKeyError): registry.get('fire') # Note that you must unregister the KEY, not the CLASS. with self.assertRaises(KeyError): registry.unregister(Squirtle) # If you try to unregister a key that isn't registered, you'll # get an error. with self.assertRaises(KeyError): registry.unregister('fire') def test_constructor_params(self): """ Params can be passed to the registered class' constructor. """ registry = ClassRegistry(attr_name='element') registry.register(Bulbasaur) # Goofus uses positional arguments, which are magical and make his code # more difficult to read. goofus = registry.get('grass', 'goofus') # Gallant uses keyword arguments, producing self-documenting code and # being courteous to his fellow developers. # He still names his pokémon after himself, though. Narcissist. gallant = registry.get('grass', name='gallant') self.assertIsInstance(goofus, Bulbasaur) self.assertEqual(goofus.name, 'goofus') self.assertIsInstance(gallant, Bulbasaur) self.assertEqual(gallant.name, 'gallant') def test_new_instance_every_time(self): """ Every time a registered class is invoked, a new instance is returned. """ registry = ClassRegistry(attr_name='element') registry.register(Wartortle) self.assertIsNot(registry['water'], registry['water']) def test_register_function(self): """ Functions can be registered as well (so long as they walk and quack like a class). """ registry = ClassRegistry() @registry.register('fire') def pokemon_factory(name=None): return Charmeleon(name=name) poke = registry.get('fire', name='trogdor') self.assertIsInstance(poke, Charmeleon) self.assertEqual(poke.name, 'trogdor') def test_contains_when_class_init_requires_arguments(self): """ Special case when checking if a class is registered, and that class' initializer requires arguments. """ registry = ClassRegistry(attr_name='element') @registry.register class Butterfree(Pokemon): element = 'bug' def __init__(self, name): super(Butterfree, self).__init__(name) self.assertTrue('bug' in registry) class GenLookupKeyTestCase(TestCase): """ Checks that a ClassRegistry subclass behaves correctly when it overrides the `gen_lookup_key` method. """ class TestRegistry(ClassRegistry): @staticmethod def gen_lookup_key(key: str) -> str: """ Simple override of `gen_lookup_key`, to ensure the registry behaves as expected when the lookup key is different. """ return ''.join(reversed(key)) def setUp(self) -> None: self.registry = self.TestRegistry() self.registry.register('fire')(Charmander) self.registry.register('water')(Squirtle) def test_contains(self): self.assertTrue('fire' in self.registry) self.assertFalse('erif' in self.registry) def test_dir(self): self.assertListEqual(dir(self.registry), ['fire', 'water']) def test_getitem(self): self.assertIsInstance(self.registry['fire'], Charmander) def test_iter(self): generator = iter(self.registry) self.assertEqual(next(generator), 'fire') self.assertEqual(next(generator), 'water') with self.assertRaises(StopIteration): next(generator) def test_len(self): self.assertEqual(len(self.registry), 2) def test_get_class(self): self.assertIs(self.registry.get_class('fire'), Charmander) def test_get(self): self.assertIsInstance(self.registry.get('fire'), Charmander) def test_items(self): generator = self.registry.items() self.assertEqual(next(generator), ('fire', Charmander)) self.assertEqual(next(generator), ('water', Squirtle)) with self.assertRaises(StopIteration): next(generator) def test_keys(self): generator = self.registry.keys() self.assertEqual(next(generator), 'fire') self.assertEqual(next(generator), 'water') with self.assertRaises(StopIteration): next(generator) def test_delitem(self): del self.registry['fire'] self.assertListEqual(list(self.registry.keys()), ['water']) def test_setitem(self): self.registry['grass'] = Bulbasaur self.assertListEqual(list(self.registry.keys()), ['fire', 'water', 'grass']) def test_unregister(self): self.registry.unregister('fire') self.assertListEqual(list(self.registry.keys()), ['water']) def test_use_case_aliases(self): """ A common use case for overriding `gen_lookup_key` is to specify some aliases (e.g., for backwards-compatibility when refactoring an existing registry). """ class TestRegistry(ClassRegistry): @staticmethod def gen_lookup_key(key: str) -> str: """ Simulate a scenario where we renamed the key for a class in the registry, but we want to preserve backwards-compatibility with existing code that hasn't been updated yet. """ if key == 'bird': return 'flying' return key registry = TestRegistry() @registry.register('flying') class MissingNo(Pokemon): pass self.assertIsInstance(registry['bird'], MissingNo) self.assertIsInstance(registry['flying'], MissingNo) self.assertListEqual(list(registry.keys()), ['flying']) class SortedClassRegistryTestCase(TestCase): def test_sort_key(self): """ When iterating over a SortedClassRegistry, classes are returned in sorted order rather than inclusion order. """ registry = SortedClassRegistry( attr_name='element', sort_key='weight', ) @registry.register class Geodude(Pokemon): element = 'rock' weight = 100 @registry.register class Machop(Pokemon): element = 'fighting' weight = 75 @registry.register class Bellsprout(Pokemon): element = 'grass' weight = 15 # The registry iterates over registered classes in ascending order by # ``weight``. self.assertListEqual( list(registry.values()), [Bellsprout, Machop, Geodude], ) def test_sort_key_reverse(self): """ Reversing the order of a sort key. """ registry = SortedClassRegistry( attr_name='element', sort_key='weight', reverse=True, ) @registry.register class Geodude(Pokemon): element = 'rock' weight = 100 @registry.register class Machop(Pokemon): element = 'fighting' weight = 75 @registry.register class Bellsprout(Pokemon): element = 'grass' weight = 15 # The registry iterates over registered classes in descending order by # ``weight``. self.assertListEqual( list(registry.values()), [Geodude, Machop, Bellsprout], ) def test_cmp_to_key(self): """ If you want to use a ``cmp`` function to define the ordering, you must use the :py:func:`cmp_to_key` function. """ def compare_pokemon(a, b): """ Sort in descending order by popularity. :param a: Tuple of (key, class, lookup_key) :param b: Tuple of (key, class, lookup_key) """ return ( (a[1].popularity < b[1].popularity) - (a[1].popularity > b[1].popularity) ) registry = SortedClassRegistry( attr_name='element', sort_key=cmp_to_key(compare_pokemon), ) @registry.register class Onix(Pokemon): element = 'rock' popularity = 50 @registry.register class Cubone(Pokemon): element = 'water' popularity = 100 @registry.register class Exeggcute(Pokemon): element = 'grass' popularity = 10 # The registry iterates over registered classes in descending order by # ``popularity``. self.assertListEqual( list(registry.values()), [Cubone, Onix, Exeggcute], ) def test_gen_lookup_key_overridden(self): """ When a ``SortedClassRegistry`` overrides the ``gen_lookup_key`` method, it can sort by lookup keys if desired. """ def compare_by_lookup_key(a, b): """ :param a: Tuple of (key, class, lookup_key) :param b: Tuple of (key, class, lookup_key) """ return (a[2] > b[2]) - (a[2] < b[2]) class TestRegistry(SortedClassRegistry): @staticmethod def gen_lookup_key(key: str) -> str: """ Simple override of `gen_lookup_key`, to ensure the sorting behaves as expected when the lookup key is different. """ return ''.join(reversed(key)) registry = TestRegistry(sort_key=cmp_to_key(compare_by_lookup_key)) registry.register('fire')(Charmander) registry.register('grass')(Bulbasaur) registry.register('water')(Squirtle) self.assertListEqual( list(registry.items()), [('fire', Charmander), ('water', Squirtle), ('grass', Bulbasaur)] ) class-registry-4.1.0/test/test_patcher.py0000644000175000017510000000450414507661043020045 0ustar nileshnileshfrom unittest import TestCase from class_registry.patcher import RegistryPatcher from class_registry.registry import ClassRegistry, RegistryKeyError from test import Bulbasaur, Charmander, Charmeleon, Ivysaur, Squirtle class RegistryPatcherTestCase(TestCase): def setUp(self): super(RegistryPatcherTestCase, self).setUp() self.registry = ClassRegistry(attr_name='element', unique=True) def test_patch_detect_keys(self): """ Patching a registry in a context, with registry keys detected automatically. """ self.registry.register(Charmander) self.registry.register(Squirtle) with RegistryPatcher(self.registry, Charmeleon, Bulbasaur): self.assertIsInstance(self.registry['fire'], Charmeleon) self.assertIsInstance(self.registry['water'], Squirtle) # Nesting contexts? You betcha! with RegistryPatcher(self.registry, Ivysaur): self.assertIsInstance(self.registry['grass'], Ivysaur) self.assertIsInstance(self.registry['grass'], Bulbasaur) # Save file corrupted. Restoring previous save... self.assertIsInstance(self.registry['fire'], Charmander) self.assertIsInstance(self.registry['water'], Squirtle) with self.assertRaises(RegistryKeyError): self.registry.get('grass') def test_patch_manual_keys(self): """ Patching a registry in a context, specifying registry keys manually. """ self.registry.register('sparky')(Charmander) self.registry.register('chad')(Squirtle) with RegistryPatcher(self.registry, sparky=Charmeleon, rex=Bulbasaur): self.assertIsInstance(self.registry['sparky'], Charmeleon) self.assertIsInstance(self.registry['chad'], Squirtle) # Don't worry Chad; your day will come! with RegistryPatcher(self.registry, rex=Ivysaur): self.assertIsInstance(self.registry['rex'], Ivysaur) self.assertIsInstance(self.registry['rex'], Bulbasaur) # Save file corrupted. Restoring previous save... self.assertIsInstance(self.registry['sparky'], Charmander) self.assertIsInstance(self.registry['chad'], Squirtle) with self.assertRaises(RegistryKeyError): self.registry.get('jodie') class-registry-4.1.0/test/test_entry_points.py0000644000175000017510000000706214507661043021156 0ustar nileshnileshfrom importlib.metadata import entry_points from unittest import TestCase from class_registry import EntryPointClassRegistry, RegistryKeyError from test import Bulbasaur, Charmander, Mew, PokemonFactory, Squirtle from test.helper import DummyDistributionFinder def setUpModule(): # Inject a distribution that defines some entry points. DummyDistributionFinder.install() def tearDownModule(): DummyDistributionFinder.uninstall() class EntryPointClassRegistryTestCase(TestCase): def test_happy_path(self): """ Loading classes automatically via entry points. See ``dummy_package.egg-info/entry_points.txt`` for more info. """ registry = EntryPointClassRegistry('pokemon') fire = registry['fire'] self.assertIsInstance(fire, Charmander) self.assertIsNone(fire.name) grass = registry.get('grass') self.assertIsInstance(grass, Bulbasaur) self.assertIsNone(grass.name) water = registry.get('water', name='archibald') self.assertIsInstance(water, Squirtle) self.assertEqual(water.name, 'archibald') # The 'psychic' entry point actually references a function, but it # works exactly the same as a class. psychic = registry.get('psychic', 'snuggles') self.assertIsInstance(psychic, Mew) self.assertEqual(psychic.name, 'snuggles') def test_branding(self): """ Configuring the registry to "brand" each class/instance with its corresponding key. """ registry = EntryPointClassRegistry('pokemon', attr_name='poke_type') try: # Branding is applied immediately to each registered class. self.assertEqual(getattr(Charmander, 'poke_type'), 'fire') self.assertEqual(getattr(Squirtle, 'poke_type'), 'water') # Instances, too! self.assertEqual(registry['fire'].poke_type, 'fire') self.assertEqual(registry.get('water', 'phil').poke_type, 'water') # Registered functions and methods can't be branded this way, # though... self.assertFalse( hasattr(PokemonFactory.create_psychic_pokemon, 'poke_type'), ) # ... but we can brand the resulting instances. self.assertEqual(registry['psychic'].poke_type, 'psychic') self.assertEqual(registry.get('psychic').poke_type, 'psychic') finally: # Clean up after ourselves. for cls in registry.values(): if isinstance(cls, type): try: delattr(cls, 'poke_type') except AttributeError: pass def test_len(self): """ Getting the length of an entry point class registry. """ # Just in case some other package defines pokémon entry # points (: expected = len(list(entry_points(group='pokemon'))) # Quick sanity check, to make sure our test pokémon are # registered correctly. self.assertGreaterEqual(expected, 4) registry = EntryPointClassRegistry('pokemon') self.assertEqual(len(registry), expected) def test_error_wrong_group(self): """ The registry can't find entry points associated with the wrong group. """ # Pokémon get registered (unsurprisingly) under the ``pokemon`` group, # not ``random``. registry = EntryPointClassRegistry('random') with self.assertRaises(RegistryKeyError): registry.get('fire') class-registry-4.1.0/test/test_cache.py0000644000175000017510000000536214507661043017465 0ustar nileshnileshfrom unittest import TestCase from class_registry.cache import ClassRegistryInstanceCache from class_registry.registry import ClassRegistry from test import Bulbasaur, Charmander, Charmeleon, Squirtle, Wartortle class ClassRegistryInstanceCacheTestCase(TestCase): def setUp(self): super(ClassRegistryInstanceCacheTestCase, self).setUp() self.registry = ClassRegistry(attr_name='element') self.cache = ClassRegistryInstanceCache(self.registry) def test_get(self): """ When an instance is returned from :py:meth:`ClassRegistryInstanceCache.get`, future invocations return the same instance. """ # Register a few classes with the ClassRegistry. self.registry.register(Bulbasaur) self.registry.register(Charmander) self.registry.register(Squirtle) poke_1 = self.cache['grass'] self.assertIsInstance(poke_1, Bulbasaur) # Same key = exact same instance. poke_2 = self.cache['grass'] self.assertIs(poke_2, poke_1) poke_3 = self.cache['water'] self.assertIsInstance(poke_3, Squirtle) # If we pull a class directly from the wrapped registry, we get # a new instance. poke_4 = self.registry['water'] self.assertIsInstance(poke_4, Squirtle) self.assertIsNot(poke_3, poke_4) def test_template_args(self): """ Extra params passed to the cache constructor are passed to the template function when creating new instances. """ self.registry.register(Charmeleon) self.registry.register(Wartortle) # Add an extra init param to the cache. cache = ClassRegistryInstanceCache(self.registry, name='Bruce') # The cache parameters are automatically applied to the class' # initializer. poke_1 = cache['fire'] self.assertIsInstance(poke_1, Charmeleon) self.assertEqual(poke_1.name, 'Bruce') poke_2 = cache['water'] self.assertIsInstance(poke_2, Wartortle) self.assertEqual(poke_2.name, 'Bruce') def test_iterator(self): """ Creating an iterator using :py:func:`iter`. """ self.registry.register(Bulbasaur) self.registry.register(Charmander) self.registry.register(Squirtle) # The cache's iterator only operates over cached instances. self.assertListEqual(list(iter(self.cache)), []) poke_1 = self.cache['water'] poke_2 = self.cache['grass'] poke_3 = self.cache['fire'] # The order that values are yielded depends on the ordering of # the wrapped registry. self.assertListEqual( list(iter(self.cache)), [poke_2, poke_3, poke_1], ) class-registry-4.1.0/test/test_auto_register.py0000644000175000017510000000523114507661043021271 0ustar nileshnileshfrom abc import ABCMeta, abstractmethod as abstract_method from unittest import TestCase from class_registry import AutoRegister, ClassRegistry class AutoRegisterTestCase(TestCase): def test_auto_register(self): """ Using :py:func:`AutoRegister` to, well, auto-register classes. """ registry = ClassRegistry(attr_name='element') # Note that we declare :py:func:`AutoRegister` as the metaclass # for our base class. class BasePokemon(metaclass=AutoRegister(registry)): """ Abstract base class; will not get registered. """ @abstract_method def get_abilities(self): raise NotImplementedError() class Sandslash(BasePokemon): """ Non-abstract subclass; will get registered automatically. """ element = 'ground' def get_abilities(self): return ['sand veil'] class BaseEvolvingPokemon(BasePokemon, metaclass=ABCMeta): """ Abstract subclass; will not get registered. """ @abstract_method def evolve(self): raise NotImplementedError() class Ekans(BaseEvolvingPokemon): """ Non-abstract subclass; will get registered automatically. """ element = 'poison' def get_abilities(self): return ['intimidate', 'shed skin'] def evolve(self): return 'Congratulations! Your EKANS evolved into ARBOK!' self.assertListEqual( list(registry.items()), [ # Note that only non-abstract classes got registered. ('ground', Sandslash), ('poison', Ekans), ], ) def test_abstract_strict_definition(self): """ If a class has no unimplemented abstract methods, it gets registered. """ registry = ClassRegistry(attr_name='element') class FightingPokemon(metaclass=AutoRegister(registry)): element = 'fighting' self.assertListEqual( list(registry.items()), [ # :py:class:`FightingPokemon` does not define any # abstract methods, so it is not considered to be # abstract! ('fighting', FightingPokemon), ], ) def test_error_attr_name_missing(self): """ The registry doesn't have an ``attr_name``. """ registry = ClassRegistry() with self.assertRaises(ValueError): AutoRegister(registry) class-registry-4.1.0/test/helper.py0000644000175000017510000000265414507661043016643 0ustar nileshnileshimport sys from importlib.metadata import DistributionFinder, PathDistribution from os import path from pathlib import Path class DummyDistributionFinder(DistributionFinder): """ Injects a dummy distribution into the meta path finder, so that we can pretend like it's been pip installed during unit tests (i.e., so that we can test ``EntryPointsClassRegistry``), without polluting the persistent virtualenv. """ DUMMY_PACKAGE_DIR = 'dummy_package.egg-info' @classmethod def install(cls): for finder in sys.meta_path: if isinstance(finder, cls): # If we've already installed an instance of the class, then # something is probably wrong with our tests. raise ValueError(f'{cls.__name__} is already installed') sys.meta_path.append(cls()) @classmethod def uninstall(cls): for i, finder in enumerate(sys.meta_path): if isinstance(finder, cls): sys.meta_path.pop(i) return else: # If we didn't find an installed instance of the class, then # something is probably wrong with our tests. raise ValueError(f'{cls.__name__} was not installed') def find_distributions(self, context=...) -> list[PathDistribution]: return [PathDistribution( Path(path.join(path.dirname(__file__), self.DUMMY_PACKAGE_DIR)) )] class-registry-4.1.0/test/dummy_package.egg-info/0000755000175000017510000000000014507661043021303 5ustar nileshnileshclass-registry-4.1.0/test/dummy_package.egg-info/top_level.txt0000644000175000017510000000000014507661043024023 0ustar nileshnileshclass-registry-4.1.0/test/dummy_package.egg-info/requires.txt0000644000175000017510000000000014507661043023671 0ustar nileshnileshclass-registry-4.1.0/test/dummy_package.egg-info/entry_points.txt0000644000175000017510000000020314507661043024574 0ustar nileshnilesh[pokemon] fire = test:Charmander grass = test:Bulbasaur psychic = test:PokemonFactory.create_psychic_pokemon water = test:Squirtle class-registry-4.1.0/test/dummy_package.egg-info/dependency_links.txt0000644000175000017510000000000114507661043025351 0ustar nileshnilesh class-registry-4.1.0/test/dummy_package.egg-info/SOURCES.txt0000644000175000017510000000000014507661043023155 0ustar nileshnileshclass-registry-4.1.0/test/dummy_package.egg-info/PKG-INFO0000644000175000017510000000056714507661043022410 0ustar nileshnileshMetadata-Version: 1.1 Name: dummy_package Version: 1.0.0 Summary: Dummy package loaded during unit tests for EntryPointClassRegistry. Home-page: https://class-registry.readthedocs.io/ Author: Phoenix Zerin Author-email: phoenix.zerin@eflglobal.com License: MIT Description: Dummy package loaded during unit tests for EntryPointClassRegistry. Keywords: test Platform: UNKNOWN class-registry-4.1.0/test/__init__.py0000644000175000017510000000163014507661043017114 0ustar nileshnileshclass Pokemon(object): """ A basic class with some attributes that we can use to test out class registries. """ element = None def __init__(self, name=None): super(Pokemon, self).__init__() self.name = name # Define some classes that we can register. class Charmander(Pokemon): element = 'fire' class Charmeleon(Pokemon): element = 'fire' class Squirtle(Pokemon): element = 'water' class Wartortle(Pokemon): element = 'water' class Bulbasaur(Pokemon): element = 'grass' class Ivysaur(Pokemon): element = 'grass' class Mew(Pokemon): element = 'psychic' class PokemonFactory(object): """ A factory that can produce new pokémon on demand. Used to test how registries behave when a function is registered instead of a class. """ @classmethod def create_psychic_pokemon(cls, name=None): return Mew(name) class-registry-4.1.0/setup.py0000644000175000017510000000016614507661043015541 0ustar nileshnileshfrom setuptools import setup if __name__ == '__main__': # :see: https://stackoverflow.com/a/62983901 setup() class-registry-4.1.0/pyproject.toml0000644000175000017510000000225614507661043016745 0ustar nileshnilesh# :see: https://peps.python.org/pep-0621/ [project] name = "phx-class-registry" version = "4.1.0" description = "Factory+Registry pattern for Python classes" readme = "README.rst" requires-python = ">= 3.10" license = { file = "LICENCE.txt" } authors = [ { email = "Phoenix Zerin " } ] keywords = [ "design pattern", "factory pattern", "registry pattern", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries :: Python Modules", ] [project.optional-dependencies] "build-system" = ["build", "twine"] "docs-builder" = ["sphinx", "sphinx_rtd_theme"] "test-runner" = ["tox"] [project.urls] Documentation = "https://class-registry.readthedocs.io/" Changelog = "https://github.com/todofixthis/class-registry/releases" Issues = "https://github.com/todofixthis/class-registry/issues" Repository = "https://github.com/todofixthis/class-registry" class-registry-4.1.0/docs/0000755000175000017510000000000014507661043014754 5ustar nileshnileshclass-registry-4.1.0/docs/requirements.txt0000644000175000017510000000014414507661043020237 0ustar nileshnilesh# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html#id6 sphinx sphinx_rtd_theme class-registry-4.1.0/docs/make.bat0000644000175000017510000000145314507661043016364 0ustar nileshnilesh@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=ClassRegistry if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd class-registry-4.1.0/docs/iterating_over_registries.rst0000644000175000017510000001023614507661043022771 0ustar nileshnileshIterating Over Registries ========================= Sometimes, you want to iterate over all of the classes registered in a :py:class:`ClassRegistry`. There are three methods included to help you do this: - :py:meth:`items` iterates over the registry keys and corresponding classes as tuples. - :py:meth:`keys` iterates over the registry keys. - :py:meth:`values` iterates over the registered classes. .. note:: Regardless of which version of Python you are using, all three of these methods return generators. Here's an example: .. code-block:: python from class_registry import ClassRegistry pokedex = ClassRegistry('element') @pokedex.register class Geodude(object): element = 'rock' @pokedex.register class Machop(object): element = 'fighting' @pokedex.register class Bellsprout(object): element = 'grass' assert list(pokedex.items()) == \ [('rock', Geodude), ('fighting', Machop), ('grass', Bellsprout)] assert list(pokedex.keys()) == ['rock', 'fighting', 'grass'] assert list(pokedex.values()) == [Geodude, Machop, Bellsprout] .. tip:: Tired of having to add the :py:meth:`register` decorator to every class? You can use the :py:func:`AutoRegister` metaclass to automatically register all non-abstract subclasses of a particular base class. See :doc:`advanced_topics` for more information. Changing the Sort Order ----------------------- As you probably noticed, these functions iterate over classes in the order that they are registered. If you'd like to customize this ordering, use :py:class:`SortedClassRegistry`: .. code-block:: python from class_registry import SortedClassRegistry pokedex =\ SortedClassRegistry(attr_name='element', sort_key='weight') @pokedex.register class Geodude(object): element = 'rock' weight = 1000 @pokedex.register class Machop(object): element = 'fighting' weight = 75 @pokedex.register class Bellsprout(object): element = 'grass' weight = 15 assert list(pokedex.items()) == \ [('grass', Bellsprout), ('fighting', Machop), ('rock', Geodude)] assert list(pokedex.keys()) == ['grass', 'fighting', 'rock'] assert list(pokedex.values()) == [Bellsprout, Machop, Geodude] In the above example, the code iterates over registered classes in ascending order by their ``weight`` attributes. You can provide a sorting function instead, if you need more control over how the items are sorted: .. code-block:: python from functools import cmp_to_key def sorter(a, b): """ Sorts items by weight, using registry key as a tiebreaker. :param a: Tuple of (key, class) :param b: Tuple of (key, class) """ # Sort descending by weight first. weight_cmp = ( (a[1].weight < b[1].weight) - (a[1].weight > b[1].weight) ) if weight_cmp != 0: return weight_cmp # Use registry key as a fallback. return ((a[0] > b[0]) - (a[0] < b[0])) pokedex =\ SortedClassRegistry( attr_name = 'element', # Note that we pass ``sorter`` through ``cmp_to_key`` first! sort_key = cmp_to_key(sorter), ) @pokedex.register class Horsea(object): element = 'water' weight = 5 @pokedex.register class Koffing(object): element = 'poison' weight = 20 @pokedex.register class Voltorb(object): element = 'electric' weight = 5 assert list(pokedex.items()) == \ [('poison', Koffing), ('electric', Voltorb), ('water', Horsea)] assert list(pokedex.keys()) == ['poison', 'electric', 'water'] assert list(pokedex.values()) == [Koffing, Voltorb, Horsea] This time, the :py:class:`SortedClassRegistry` used our custom sorter function, so that the classes were sorted descending by weight, with the registry key used as a tiebreaker. .. important:: Note that we had to pass the sorter function through :py:func:`functools.cmp_to_key` before providing it to the :py:class:`SortedClassRegistry` initializer. This is necessary because of how sorting works in Python. See `Sorting HOW TO`_ for more information. .. _sorting how to: https://docs.python.org/3/howto/sorting.html#key-functions class-registry-4.1.0/docs/index.rst0000644000175000017510000000513614507661043016622 0ustar nileshnileshClassRegistry ============= .. toctree:: :maxdepth: 1 :caption: Contents: getting_started factories_vs_registries iterating_over_registries entry_points advanced_topics ClassRegistry ============= At the intersection of the Registry and Factory patterns lies the ``ClassRegistry``: - Define global factories that generate new class instances based on configurable keys. - Seamlessly create powerful service registries. - Integrate with setuptools's ``entry_points`` system to make your registries infinitely extensible by 3rd-party libraries! - And more! Getting Started --------------- Create a registry using the ``class_registry.ClassRegistry`` class, then decorate any classes that you wish to register with its ``register`` method: .. code-block:: python from class_registry import ClassRegistry pokedex = ClassRegistry() @pokedex.register('fire') class Charizard(Pokemon): ... @pokedex.register('grass') class Bulbasaur(Pokemon): ... @pokedex.register('water') class Squirtle(Pokemon): ... To create a class instance from a registry, use the subscript operator: .. code-block:: python # Charizard, I choose you! fighter1 = pokedex['fire'] # CHARIZARD fainted! # How come my rival always picks the type that my pokémon is weak against?? fighter2 = pokedex['grass'] Advanced Usage -------------- There's a whole lot more you can do with ClassRegistry, including: - Provide args and kwargs to new class instances. - Automatically register non-abstract classes. - Integrate with setuptools's ``entry_points`` system so that 3rd-party libraries can add their own classes to your registries. - Wrap your registry in an instance cache to create a service registry. - And more! To learn more about what you can do with ClassRegistry, :doc:`keep reading! ` Requirements ------------ ClassRegistry is known to be compatible with the following Python versions: - 3.12 - 3.11 - 3.10 .. note:: I'm only one person, so to keep from getting overwhelmed, I'm only committing to supporting the 3 most recent versions of Python. ClassRegistry's code is pretty simple, so it's likely to be compatible with versions not listed here; there just won't be any test coverage to prove it 😇 Installation ------------ Install the latest stable version via pip:: pip install phx-class-registry .. important:: Make sure to install `phx-class-registry`, **not** `class-registry`. I created the latter at a previous job years ago, and after I left they never touched that project again — so in the end I had to fork it 🤷 class-registry-4.1.0/docs/getting_started.rst0000644000175000017510000001044214507661043020676 0ustar nileshnileshGetting Started =============== As you saw in the :doc:`introduction `, you can create a new registry using the :py:class:`class_registry.ClassRegistry` class. :py:class:`ClassRegistry` defines a ``register`` method that you can use as a decorator to add classes to the registry: .. code-block:: python from class_registry import ClassRegistry pokedex = ClassRegistry() @pokedex.register('fire') class Charizard(object): pass Once you've registered a class, you can then create a new instance using the corresponding registry key: .. code-block:: python sparky = pokedex['fire'] assert isinstance(sparky, Charizard) Note in the above example that ``sparky`` is an `instance` of ``Charizard``. If you try to access a registry key that has no classes registered, it will raise a :py:class:`class_registry.RegistryKeyError`: .. code-block:: python from class_registry import RegistryKeyError try: tex = pokedex['spicy'] except RegistryKeyError: pass Registry Keys ------------- By default, you have to provide the registry key whenever you register a new class. But, there's an easier way to do it! When you initialize your :py:class:`ClassRegistry`, provide an ``attr_name`` parameter. When you register new classes, your registry will automatically extract the registry key using that attribute: .. code-block:: python pokedex = ClassRegistry('element') @pokedex.register class Squirtle(object): element = 'water' beauregard = pokedex['water'] assert isinstance(beauregard, Squirtle) Note in the above example that the registry automatically extracted the registry key for the ``Squirtle`` class using its ``element`` attribute. Collisions ---------- What happens if two classes have the same registry key? .. code-block:: python pokedex = ClassRegistry('element') @pokedex.register class Bulbasaur(object): element = 'grass' @pokedex.register class Ivysaur(object): element = 'grass' janet = pokedex['grass'] assert isinstance(janet, Ivysaur) As you can see, if two (or more) classes have the same registry key, whichever one is registered last will override any of the other(s). .. note:: It is not always easy to predict the order in which classes will be registered, especially when they are spread across different modules, so you probably don't want to rely on this behaviour! If you want to prevent collisions, you can pass ``unique=True`` to the :py:class:`ClassRegistry` initializer to raise an exception whenever a collision occurs: .. code-block:: python from class_registry import RegistryKeyError pokedex = ClassRegistry('element', unique=True) @pokedex.register class Bulbasaur(object): element = 'grass' try: @pokedex.register class Ivysaur(object): element = 'grass' except RegistryKeyError: pass janet = pokedex['grass'] assert isinstance(janet, Bulbasaur) Because we passed ``unique=True`` to the :py:class:`ClassRegistry` initialiser, attempting to register ``Ivysaur`` with the same registry key as ``Bulbasaur`` raised a :py:class:`RegistryKeyError`, so it didn't override ``Bulbasaur``. Init Params ----------- Every time you access a registry key in a :py:class:`ClassRegistry`, it creates a new instance: .. code-block:: python marlene = pokedex['grass'] charlene = pokedex['grass'] assert marlene is not charlene Since you're creating a new instance every time, you also have the option of providing args and kwargs to the class initialiser using the registry's :py:meth:`get` method: .. code-block:: python pokedex = ClassRegistry('element') @pokedex.register class Caterpie(object): element = 'bug' def __init__(self, level=1): super(Caterpie, self).__init__() self.level = level timmy = pokedex.get('bug') assert timmy.level == 1 tommy = pokedex.get('bug', 16) assert tommy.level == 16 tammy = pokedex.get('bug', level=42) assert tammy.level == 42 Any arguments that you provide to :py:meth:`get` will be passed directly to the corresponding class' initialiser. .. hint:: You can create a registry that always returns the same instance per registry key by wrapping it in a :py:class:`ClassRegistryInstanceCache`. See :doc:`factories_vs_registries` for more information. class-registry-4.1.0/docs/factories_vs_registries.rst0000644000175000017510000000337714507661043022447 0ustar nileshnileshFactories vs. Registries ======================== Despite its name, :py:class:`ClassRegistry` also has aspects in common with the Factory pattern. Most notably, accessing a registry key automatically creates a new instance of the corresponding class. But, what if you want a :py:class:`ClassRegistry` to behave more strictly like a registry — always returning the the `same` instance each time the same key is accessed? This is where :py:class:`ClassRegistryInstanceCache` comes into play. It wraps a :py:class:`ClassRegistry` and provides a caching mechanism, so that each time you access a particular key, it always returns the same instance for that key. Let's see what this looks like in action: .. code-block:: python from class_registry import ClassRegistry, ClassRegistryInstanceCache pokedex = ClassRegistry('element') @pokedex.register class Pikachu(object): element = 'electric' @pokedex.register class Alakazam(object): element = 'psychic' fighters = ClassRegistryInstanceCache(pokedex) # Accessing the ClassRegistry yields a different instance every # time. pika_1 = pokedex['electric'] assert isinstance(pika_1, Pikachu) pika_2 = pokedex['electric'] assert isinstance(pika_2, Pikachu) assert pika_1 is not pika_2 # ClassRegistryInstanceCache works just like ClassRegistry, except # it returns the same instance per key. pika_3 = fighters['electric'] assert isinstance(pika_3, Pikachu) pika_4 = fighters['electric'] assert isinstance(pika_4, Pikachu) assert pika_3 is pika_4 darth_vader = fighters['psychic'] assert isinstance(darth_vader, Alakazam) anakin_skywalker = fighters['psychic'] assert isinstance(anakin_skywalker, Alakazam) assert darth_vader is anakin_skywalker class-registry-4.1.0/docs/entry_points.rst0000644000175000017510000000713514507661043020251 0ustar nileshnileshEntry Points Integration ======================== A serially-underused feature of setuptools is its `entry points`_. This feature allows you to expose a pluggable interface in your project. Other libraries can then declare entry points and inject their own classes into your class registries! Let's see what that might look like in practice. First, we'll create a package with its own ``pyproject.toml`` file: .. code-block:: toml # generation_2/pyproject.toml [project] name="pokemon-generation-2" description="Extends the pokédex with generation 2 pokémon!" [project.entry-points.pokemon] grass="gen2.pokemon:Chikorita" fire="gen2.pokemon:Cyndaquil" water="gen2.pokemon:Totodile" Note that we declared some ``pokemon`` entry points. .. tip:: If your project uses ``setup.py``, it will look like this instead: .. code-block:: python # generation_2/setup.py from setuptools import setup setup( name = 'pokemon-generation-2', description = 'Extends the pokédex with generation 2 pokémon!', entry_points = { 'pokemon': [ 'grass=gen2.pokemon:Chikorita', 'fire=gen2.pokemon:Cyndaquil', 'water=gen2.pokemon:Totodile', ], }, ) Note that ``setup.py`` is being phased out in favour of ``pyproject.toml``. `Learn more about pyproject.toml.`_ Let's see what happens once the ``pokemon-generation-2`` package is installed:: % pip install pokemon-generation-2 % ipython In [1]: from class_registry import EntryPointClassRegistry In [2]: pokedex = EntryPointClassRegistry('pokemon') In [3]: list(pokedex.items()) Out[3]: [('grass', ), ('fire', ), ('water', )] Simply declare an :py:class:`EntryPointClassRegistry` instance, and it will automatically find any classes registered to that entry point group across every single installed project in your virtualenv! Reverse Lookups --------------- From time to time, you may need to perform a "reverse lookup": Given a class or instance, you want to determine which registry key is associated with it. For :py:class:`ClassRegistry`, performing a reverse lookup is simple because the registry key is (usually) defined by an attribute on the class itself. However, :py:class:`EntryPointClassRegistry` uses an external source to define the registry keys, so it's a bit tricky to go back and find the registry key for a given class. If you would like to enable reverse lookups in your application, you can provide an optional ``attr_name`` argument to the registry's initializer, which will cause the registry to "brand" every object it returns with the corresponding registry key. .. code-block:: python In [1]: from class_registry import EntryPointClassRegistry In [2]: pokedex = EntryPointClassRegistry('pokemon', attr_name='element') In [3]: fire_pokemon = pokedex['fire'] In [4]: fire_pokemon.element Out[4]: 'fire' In [5]: water_pokemon_class = pokedex.get_class('water') In [6]: water_pokemon_class.element Out[6]: 'water' We set ``attr_name='element'`` when initializing the :py:class:`EntryPointClassRegistry`, so it set the ``element`` attribute on every class and instance that it returned. .. caution:: If a class already has an attribute with the same name, the registry will overwrite it. .. _entry points: http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins .. _Learn more about pyproject.toml.: https://stackoverflow.com/q/62983756/5568265 class-registry-4.1.0/docs/conf.py0000644000175000017510000001122614507661043016255 0ustar nileshnilesh#!/usr/bin/env python3 # class-registry documentation build configuration file, created by # sphinx-quickstart on Wed Jun 28 17:10:35 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'class-registry' copyright = '2017, Phoenix Zerin' author = 'Phoenix Zerin' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. # version = '' # The full version, including alpha/beta/rc tags. # release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en-NZ' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'class-registrydoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'class-registry.tex', 'class-registry Documentation', 'Phoenix Zerin', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'classregistry', 'class-registry Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'class-registry', 'class-registry Documentation', author, 'class-registry', 'One line description of project.', 'Miscellaneous'), ] class-registry-4.1.0/docs/advanced_topics.rst0000644000175000017510000001306014507661043020634 0ustar nileshnileshAdvanced 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) class-registry-4.1.0/docs/Makefile0000644000175000017510000000114514507661043016415 0ustar nileshnilesh# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = ClassRegistry SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)class-registry-4.1.0/class_registry/0000755000175000017510000000000014507661043017061 5ustar nileshnileshclass-registry-4.1.0/class_registry/registry.py0000644000175000017510000003602714507661043021313 0ustar nileshnileshimport typing from abc import ABCMeta, abstractmethod as abstract_method from collections import OrderedDict from functools import cmp_to_key from inspect import isclass as is_class __all__ = [ 'BaseRegistry', 'ClassRegistry', 'BaseMutableRegistry', 'RegistryKeyError', 'SortedClassRegistry', ] class RegistryKeyError(KeyError): """ Used to differentiate a registry lookup from a standard KeyError. This is especially useful when a registry class expects to extract values from dicts to generate keys. """ pass class BaseRegistry(typing.Mapping, metaclass=ABCMeta): """ Base functionality for registries. """ def __contains__(self, key: typing.Hashable) -> bool: """ Returns whether the specified key is registered. """ try: # Use :py:meth:`get_class` instead of :py:meth:`__getitem__`, to # avoid creating a new instance unnecessarily (i.e., prevent # errors if the corresponding class' constructor requires # arguments). self.get_class(key) except RegistryKeyError: return False else: return True def __dir__(self) -> typing.List[typing.Hashable]: return list(self.keys()) def __getitem__(self, key: typing.Hashable) -> object: """ Shortcut for calling :py:meth:`get` with empty args/kwargs. """ return self.get(key) def __iter__(self) -> typing.Generator[typing.Hashable, None, None]: """ Returns a generator for iterating over registry keys, in the order that they were registered. """ return self.keys() @abstract_method def __len__(self) -> int: """ Returns the number of registered classes. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) def __missing__(self, key) -> typing.Optional[object]: """ Defines what to do when trying to access an unregistered key. Default behaviour is to throw a typed exception, but you could override this in a subclass, e.g., to return a default value. """ raise RegistryKeyError(key) @abstract_method def get_class(self, key: typing.Hashable) -> type: """ Returns the class associated with the specified key. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) def get(self, key: typing.Hashable, *args, **kwargs) -> object: """ Creates a new instance of the class matching the specified key. :param key: The corresponding load key. :param args: Positional arguments passed to class initializer. Ignored if the class registry was initialized with a null template function. :param kwargs: Keyword arguments passed to class initializer. Ignored if the class registry was initialized with a null template function. References: - :py:meth:`__init__` """ return self.create_instance(self.get_class(key), *args, **kwargs) @staticmethod def gen_lookup_key(key: typing.Hashable) -> typing.Hashable: """ Used by :py:meth:`get` to generate a lookup key. You may override this method in a subclass, for example if you need to support legacy aliases, etc. """ return key @staticmethod def create_instance(class_: type, *args, **kwargs) -> object: """ Prepares the return value for :py:meth:`get`. You may override this method in a subclass, if you want to customize the way new instances are created. :param class_: The requested class. :param args: Positional keywords passed to :py:meth:`get`. :param kwargs: Keyword arguments passed to :py:meth:`get`. """ return class_(*args, **kwargs) @abstract_method def items(self) -> typing.Generator[ typing.Tuple[typing.Hashable, type], None, None]: """ Iterates over registered classes and their corresponding keys, in the order that they were registered. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) def keys(self) -> typing.Generator[typing.Hashable, None, None]: """ Returns a generator for iterating over registry keys, in the order that they were registered. """ for item in self.items(): yield item[0] def values(self) -> typing.Generator[type, None, None]: """ Returns a generator for iterating over registered classes, in the order that they were registered. """ for item in self.items(): yield item[1] class BaseMutableRegistry(BaseRegistry, typing.MutableMapping, metaclass=ABCMeta): """ Extends :py:class:`BaseRegistry` with methods that can be used to modify the registered classes. """ def __init__(self, attr_name: typing.Optional[str] = None) -> None: """ :param attr_name: If provided, :py:meth:`register` will automatically detect the key to use when registering new classes. """ super(BaseMutableRegistry, self).__init__() self.attr_name = attr_name # Map lookup keys to readable keys. # Only needed when :py:meth:`gen_lookup_key` is overridden, but I'm not # good enough at reflection black magic to figure out how to do that (: self._lookup_keys: typing.Dict[typing.Hashable, typing.Hashable] = {} def __delitem__(self, key: typing.Hashable) -> None: """ Provides alternate syntax for un-registering a class. """ self._unregister(self.gen_lookup_key(key)) del self._lookup_keys[key] def __repr__(self) -> str: return '{type}({attr_name!r})'.format( attr_name=self.attr_name, type=type(self).__name__, ) def __setitem__(self, key: typing.Hashable, class_: type) -> None: """ Provides alternate syntax for registering a class. """ lookup_key = self.gen_lookup_key(key) self._register(lookup_key, class_) self._lookup_keys[key] = lookup_key def register(self, key: typing.Union[typing.Hashable, type]) -> \ typing.Callable[[type], type]: """ Decorator that registers a class with the registry. Example:: registry = ClassRegistry(attr_name='widget_type') @registry.register class CustomWidget(BaseWidget): widget_type = 'custom' ... # Override the registry key: @registry.register('premium') class AdvancedWidget(BaseWidget): ... :param key: The registry key to use for the registered class. Optional if the registry's :py:attr:`attr_name` is set. """ # ``@register`` usage: if is_class(key): if self.attr_name: attr_key = getattr(key, self.attr_name) lookup_key = self.gen_lookup_key(attr_key) # Note that ``getattr`` will raise an AttributeError if the # class doesn't have the required attribute. self._register(lookup_key, key) self._lookup_keys[attr_key] = lookup_key return key else: raise ValueError( 'Attempting to register {cls} to {registry} via decorator,' ' but `{registry}.attr_key` is not set.'.format( cls=key.__name__, registry=type(self).__name__, ) ) # ``@register('some_attr')`` usage: def _decorator(cls: type) -> type: lookup_key = self.gen_lookup_key(key) self._register(lookup_key, cls) self._lookup_keys[key] = lookup_key return cls return _decorator def unregister(self, key: typing.Hashable) -> type: """ Unregisters the class with the specified key. :param key: The registry key to remove (not the registered class!). :return: The class that was unregistered. :raise: - :py:class:`KeyError` if the key is not registered. """ result = self._unregister(self.gen_lookup_key(key)) del self._lookup_keys[key] return result @abstract_method def _register(self, key: typing.Hashable, class_: type) -> None: """ Registers a class with the registry. :param key: Has already been processed by :py:meth:`gen_lookup_key`. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) @abstract_method def _unregister(self, key: typing.Hashable) -> type: """ Unregisters the class at the specified key. :param key: Has already been processed by :py:meth:`gen_lookup_key`. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) class ClassRegistry(BaseMutableRegistry): """ Maintains a registry of classes and provides a generic factory for instantiating them. """ def __init__( self, attr_name: typing.Optional[str] = None, unique: bool = False, ) -> None: """ :param attr_name: If provided, :py:meth:`register` will automatically detect the key to use when registering new classes. :param unique: Determines what happens when two classes are registered with the same key: - ``True``: A :py:class:`KeyError` will be raised. - ``False``: The second class will replace the first one. """ super(ClassRegistry, self).__init__(attr_name) self.unique = unique self._registry: typing.OrderedDict[ typing.Hashable, type] = OrderedDict() def __len__(self) -> int: """ Returns the number of registered classes. """ return len(self._registry) def __repr__(self) -> str: return '{type}(attr_name={attr_name!r}, unique={unique!r})'.format( attr_name=self.attr_name, type=type(self).__name__, unique=self.unique, ) def get_class(self, key: typing.Hashable) -> typing.Optional[type]: """ Returns the class associated with the specified key. """ lookup_key = self.gen_lookup_key(key) try: return self._registry[lookup_key] except KeyError: return self.__missing__(lookup_key) def items(self) -> typing.Generator[ typing.Tuple[typing.Hashable, type], None, None]: """ Iterates over all registered classes, in the order they were added. """ for key, lookup_key in self._lookup_keys.items(): yield key, self._registry[lookup_key] def _register(self, key: typing.Hashable, class_: type) -> None: """ Registers a class with the registry. :param key: Has already been processed by :py:meth:`gen_lookup_key`. """ if key in ['', None]: raise ValueError( 'Attempting to register class {cls} ' 'with empty registry key {key!r}.'.format( cls=class_.__name__, key=key, ), ) if self.unique and (key in self._registry): raise RegistryKeyError( '{cls} with key {key!r} is already registered.'.format( cls=class_.__name__, key=key, ), ) self._registry[key] = class_ def _unregister(self, key: typing.Hashable) -> type: """ Unregisters the class at the specified key. :param key: Has already been processed by :py:meth:`gen_lookup_key`. """ return ( self._registry.pop(key) if key in self._registry else self.__missing__(key) ) class SortedClassRegistry(ClassRegistry): """ A ClassRegistry that uses a function to determine sort order when iterating. """ def __init__( self, sort_key: typing.Union[ str, typing.Callable[ [ typing.Tuple[typing.Hashable, type, typing.Hashable], typing.Tuple[typing.Hashable, type, typing.Hashable] ], int, ], ], attr_name: typing.Optional[str] = None, unique: bool = False, reverse: bool = False, ) -> None: """ :param sort_key: Attribute name or callable, used to determine the sort value. If callable, must accept two tuples of (key, class, lookup_key). :param attr_name: If provided, :py:meth:`register` will automatically detect the key to use when registering new classes. :param unique: Determines what happens when two classes are registered with the same key: - ``True``: The second class will replace the first one. - ``False``: A ``ValueError`` will be raised. :param reverse: Whether to reverse the sort ordering. """ super(SortedClassRegistry, self).__init__(attr_name, unique) self._sort_key = ( sort_key if callable(sort_key) else self.create_sorter(sort_key) ) self.reverse = reverse def items(self) -> typing.Generator[ typing.Tuple[typing.Hashable, type], None, None]: for (key, class_, _) in sorted( # Provide human-readable key and lookup key to the sorter... ((key, class_, self.gen_lookup_key(key)) for (key, class_) in super().items()), key=self._sort_key, reverse=self.reverse, ): # ... but for parity with other ClassRegistry types, only include # the human-readable key in the result. yield key, class_ @staticmethod def create_sorter(sort_key: str) -> typing.Callable[ [ typing.Tuple[typing.Hashable, type], typing.Tuple[typing.Hashable, type] ], int, ]: """ Given a sort key, creates a function that can be used to sort items when iterating over the registry. """ def sorter(a, b): a_attr = getattr(a[1], sort_key) b_attr = getattr(b[1], sort_key) return (a_attr > b_attr) - (a_attr < b_attr) return cmp_to_key(sorter) class-registry-4.1.0/class_registry/patcher.py0000644000175000017510000000563214507661043021067 0ustar nileshnileshimport typing from .registry import BaseMutableRegistry, RegistryKeyError __all__ = [ 'RegistryPatcher', ] class RegistryPatcher(object): """ Creates a context in which classes are temporarily registered with a class registry, then removed when the context exits. Note: only mutable registries can be patched! """ class DoesNotExist(object): """ Used to identify a value that did not exist before we started. """ pass def __init__(self, registry: BaseMutableRegistry, *args, **kwargs) -> None: """ :param registry: A :py:class:`MutableRegistry` instance to patch. :param args: Classes to add to the registry. This behaves the same as decorating each class with ``@registry.register``. Note: ``registry.attr_name`` must be set! :param kwargs: Same as ``args``, except you explicitly specify the registry keys. In the event of a conflict, values in ``args`` override values in ``kwargs``. """ super(RegistryPatcher, self).__init__() for class_ in args: kwargs[getattr(class_, registry.attr_name)] = class_ self.target = registry self._new_values = kwargs self._prev_values = {} def __enter__(self) -> None: self.apply() def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.restore() def apply(self) -> None: """ Applies the new values. """ # Back up previous values. self._prev_values = { key: self._get_value(key, self.DoesNotExist) for key in self._new_values } # Patch values. for key, value in self._new_values.items(): # Remove the existing value first (prevents issues if the registry # has ``unique=True``). self._del_value(key) if value is not self.DoesNotExist: self._set_value(key, value) def restore(self) -> None: """ Restores previous settings. """ # Restore previous settings. for key, value in self._prev_values.items(): # Remove the existing value first (prevents issues if the registry # has ``unique=True``). self._del_value(key) if value is not self.DoesNotExist: self._set_value(key, value) def _get_value(self, key: typing.Hashable, default=None) -> \ typing.Optional[type]: try: return self.target.get_class(key) except RegistryKeyError: return default def _set_value(self, key: typing.Hashable, value: type) -> None: self.target.register(key)(value) def _del_value(self, key: typing.Hashable) -> None: try: self.target.unregister(key) except RegistryKeyError: pass class-registry-4.1.0/class_registry/entry_points.py0000644000175000017510000000635214507661043022176 0ustar nileshnileshfrom importlib.metadata import entry_points import typing from . import BaseRegistry __all__ = [ 'EntryPointClassRegistry', ] class EntryPointClassRegistry(BaseRegistry): """ A class registry that loads classes using setuptools entry points. """ def __init__( self, group: str, attr_name: typing.Optional[str] = None, ) -> None: """ :param group: The name of the entry point group that will be used to load new classes. :param attr_name: If set, the registry will "brand" each class with its corresponding registry key. This makes it easier to perform reverse lookups later. Note: if a class already defines this attribute, the registry will overwrite it! """ super(EntryPointClassRegistry, self).__init__() self.attr_name = attr_name self.group = group self._cache: typing.Optional[typing.Dict[typing.Hashable, type]] = None """ Caches registered classes locally, so that we don't have to keep iterating over entry points. """ # If :py:attr:`attr_name` is set, warm the cache immediately, to apply # branding. if self.attr_name: self._get_cache() def __len__(self) -> int: return len(self._get_cache()) def __repr__(self) -> str: return '{type}(group={group!r})'.format( group=self.group, type=type(self).__name__, ) def get(self, key: typing.Hashable, *args, **kwargs) -> object: instance = super(EntryPointClassRegistry, self).get(key, *args, **kwargs) if self.attr_name: # Apply branding to the instance explicitly. # This is particularly important if the corresponding entry point # references a function or method. setattr(instance, self.attr_name, key) return instance def get_class(self, key: typing.Hashable) -> typing.Optional[type]: try: cls = self._get_cache()[key] except KeyError: cls = self.__missing__(key) return cls def items(self) -> typing.ItemsView[typing.Hashable, type]: return self._get_cache().items() def refresh(self): """ Purges the local cache. The next access attempt will reload all entry points. This is useful if you load a distribution at runtime... such as during unit tests for class-registry. Otherwise, it probably serves no useful purpose """ self._cache = None def _get_cache(self) -> typing.Dict[typing.Hashable, type]: """ Populates the cache (if necessary) and returns it. """ if self._cache is None: self._cache = {} for e in entry_points(group=self.group): cls = e.load() # Try to apply branding, but only for compatible types (i.e., # functions and methods can't be branded this way). if self.attr_name and isinstance(cls, type): setattr(cls, self.attr_name, e.name) self._cache[e.name] = cls return self._cache class-registry-4.1.0/class_registry/cache.py0000644000175000017510000000705214507661043020502 0ustar nileshnileshimport typing from collections import defaultdict from . import ClassRegistry __all__ = [ 'ClassRegistryInstanceCache', ] class ClassRegistryInstanceCache(object): """ Wraps a ClassRegistry instance, caching instances as they are created. This allows you to create [multiple] registries that cache INSTANCES locally (so that they can be scoped and garbage-collected), while keeping the CLASS registry separate. Note that the internal class registry is copied by reference, so any classes that are registered afterward are accessible to both the ClassRegistry and the ClassRegistryInstanceCache. """ def __init__(self, class_registry: ClassRegistry, *args, **kwargs) -> None: """ :param class_registry: The wrapped ClassRegistry. :param args: Positional arguments passed to the class registry's template when creating new instances. :param kwargs: Keyword arguments passed to the class registry's template function when creating new instances. """ super(ClassRegistryInstanceCache, self).__init__() self._registry = class_registry self._cache: typing.Dict[typing.Hashable, type] = {} self._key_map: typing.Dict[typing.Hashable, list] = defaultdict(list) self._template_args = args self._template_kwargs = kwargs def __getitem__(self, key): """ Returns the cached instance associated with the specified key. """ instance_key = self.get_instance_key(key) if instance_key not in self._cache: class_key = self.get_class_key(key) # Map lookup keys to cache keys so that we can iterate over them in # the correct order. self._key_map[class_key].append(instance_key) self._cache[instance_key] = self._registry.get( class_key, *self._template_args, **self._template_kwargs ) return self._cache[instance_key] def __iter__(self) -> typing.Generator[type, None, None]: """ Returns a generator for iterating over cached instances, using the wrapped registry to determine sort order. If a key has not been accessed yet, it will not be included. """ for lookup_key in self._registry.keys(): for cache_key in self._key_map[lookup_key]: yield self._cache[cache_key] def warm_cache(self) -> None: """ Warms up the cache, ensuring that an instance is created for every key currently in the registry. .. note:: This method does not account for any classes that may be added to the wrapped ClassRegistry in the future. """ for key in self._registry.keys(): self.__getitem__(key) def get_instance_key(self, key: typing.Hashable) -> typing.Hashable: """ Generates a key that can be used to store/lookup values in the instance cache. :param key: Value provided to :py:meth:`__getitem__`. """ return self.get_class_key(key) def get_class_key(self, key: typing.Hashable) -> typing.Hashable: """ Generates a key that can be used to store/lookup values in the wrapped :py:class:`ClassRegistry` instance. This method is only invoked in the event of a cache miss. :param key: Value provided to :py:meth:`__getitem__`. """ return self._registry.gen_lookup_key(key) class-registry-4.1.0/class_registry/auto_register.py0000644000175000017510000000324114507661043022307 0ustar nileshnileshfrom abc import ABCMeta from inspect import isabstract as is_abstract from .registry import BaseMutableRegistry __all__ = [ 'AutoRegister', ] def AutoRegister(registry: BaseMutableRegistry, base_type: type = ABCMeta) -> type: """ Creates a metaclass that automatically registers all non-abstract subclasses in the specified registry. IMPORTANT: Python defines abstract as "having at least one unimplemented abstract method"; specifying :py:class:`ABCMeta` as the metaclass is not enough! Example:: commands = ClassRegistry(attr_name='command_name') # Specify ``AutoRegister`` as the metaclass: class BaseCommand(metaclass=AutoRegister(commands)): @abstractmethod def print(self): raise NotImplementedError() class PrintCommand(BaseCommand): command_name = 'print' def print(self): ... print(list(commands.items())) # [('print', PrintCommand)] :param registry: The registry that new classes will be added to. Note: the registry's ``attr_name`` attribute must be set! :param base_type: The base type of the metaclass returned by this function. 99.99% of the time, this should be :py:class:`ABCMeta`. """ if not registry.attr_name: raise ValueError( 'Missing `attr_name` in {registry}.'.format(registry=registry), ) class _metaclass(base_type): def __init__(self, what, bases=None, attrs=None): super(_metaclass, self).__init__(what, bases, attrs) if not is_abstract(self): registry.register(self) return _metaclass class-registry-4.1.0/class_registry/__init__.py0000644000175000017510000000017514507661043021175 0ustar nileshnileshfrom .registry import * from .auto_register import * from .cache import * from .entry_points import * from .patcher import * class-registry-4.1.0/README.rst0000644000175000017510000001275514507661043015525 0ustar nileshnilesh.. image:: https://github.com/todofixthis/class-registry/actions/workflows/build.yml/badge.svg :target: https://github.com/todofixthis/class-registry/actions/workflows/build.yml .. image:: https://readthedocs.org/projects/class-registry/badge/?version=latest :target: http://class-registry.readthedocs.io/ ClassRegistry ============= At the intersection of the Registry and Factory patterns lies the ``ClassRegistry``: - Define global factories that generate new class instances based on configurable keys. - Seamlessly create powerful service registries. - Integrate with setuptools's ``entry_points`` system to make your registries infinitely extensible by 3rd-party libraries! - And more! Getting Started --------------- Create a registry using the ``class_registry.ClassRegistry`` class, then decorate any classes that you wish to register with its ``register`` method: .. code-block:: python from class_registry import ClassRegistry pokedex = ClassRegistry() @pokedex.register('fire') class Charizard(Pokemon): ... @pokedex.register('grass') class Bulbasaur(Pokemon): ... @pokedex.register('water') class Squirtle(Pokemon): ... To create a class instance from a registry, use the subscript operator: .. code-block:: python # Charizard, I choose you! fighter1 = pokedex['fire'] # CHARIZARD fainted! # How come my rival always picks the type that my pokémon is weak against?? fighter2 = pokedex['grass'] Advanced Usage ~~~~~~~~~~~~~~ There's a whole lot more you can do with ClassRegistry, including: - Provide args and kwargs to new class instances. - Automatically register non-abstract classes. - Integrate with setuptools's ``entry_points`` system so that 3rd-party libraries can add their own classes to your registries. - Wrap your registry in an instance cache to create a service registry. - And more! For more advanced usage, check out the documentation on `ReadTheDocs`_! Requirements ------------ ClassRegistry is known to be compatible with the following Python versions: - 3.12 - 3.11 - 3.10 .. note:: I'm only one person, so to keep from getting overwhelmed, I'm only committing to supporting the 3 most recent versions of Python. ClassRegistry's code is pretty simple, so it's likely to be compatible with versions not listed here; there just won't be any test coverage to prove it 😇 Installation ------------ Install the latest stable version via pip:: pip install phx-class-registry .. important:: Make sure to install `phx-class-registry`, **not** `class-registry`. I created the latter at a previous job years ago, and after I left they never touched that project again and stopped responding to my emails — so in the end I had to fork it 🤷 Running Unit Tests ------------------ Install the package with the ``test-runner`` extra to set up the necessary dependencies, and then you can run the tests with the ``tox`` command:: pip install -e .[test-runner] tox -p To run tests in the current virtualenv:: python -m unittest Documentation ------------- Documentation is available on `ReadTheDocs`_. If you are installing from source (see above), you can also build the documentation locally: #. Install extra dependencies (you only have to do this once):: pip install -e '.[docs-builder]' #. Switch to the ``docs`` directory:: cd docs #. Build the documentation:: make html Releases -------- Steps to build releases are based on `Packaging Python Projects Tutorial`_ .. important:: Make sure to build releases off of the ``main`` branch, and check that all changes from ``develop`` have been merged before creating the release! 1. Build the Project ~~~~~~~~~~~~~~~~~~~~ #. Install extra dependencies (you only have to do this once):: pip install -e '.[build-system]' #. Delete artefacts from previous builds, if applicable:: rm dist/* #. Run the build:: python -m build #. The build artefacts will be located in the ``dist`` directory at the top level of the project. 2. Upload to PyPI ~~~~~~~~~~~~~~~~~ #. `Create a PyPI API token`_ (you only have to do this once). #. Increment the version number in ``pyproject.toml``. #. Check that the build artefacts are valid, and fix any errors that it finds:: python -m twine check dist/* #. Upload build artefacts to PyPI:: python -m twine upload dist/* 3. Create GitHub Release ~~~~~~~~~~~~~~~~~~~~~~~~ #. Create a tag and push to GitHub:: git tag git push ```` must match the updated version number in ``pyproject.toml``. #. Go to the `Releases page for the repo`_. #. Click ``Draft a new release``. #. Select the tag that you created in step 1. #. Specify the title of the release (e.g., ``ClassRegistry v1.2.3``). #. Write a description for the release. Make sure to include: - Credit for code contributed by community members. - Significant functionality that was added/changed/removed. - Any backwards-incompatible changes and/or migration instructions. - SHA256 hashes of the build artefacts. #. GPG-sign the description for the release (ASCII-armoured). #. Attach the build artefacts to the release. #. Click ``Publish release``. .. _Create a PyPI API token: https://pypi.org/manage/account/token/ .. _Packaging Python Projects Tutorial: https://packaging.python.org/en/latest/tutorials/packaging-projects/ .. _ReadTheDocs: https://class-registry.readthedocs.io/ .. _Releases page for the repo: https://github.com/todofixthis/class-registry/releases .. _tox: https://tox.readthedocs.io/ class-registry-4.1.0/MANIFEST.in0000644000175000017510000000006414507661043015562 0ustar nileshnileshinclude LICENCE.txt graft test prune **/__pycache__ class-registry-4.1.0/LICENCE.txt0000644000175000017510000000205314507661043015627 0ustar nileshnileshMIT Licence Copyright (c) 2017 EFL Global Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. class-registry-4.1.0/.readthedocs.yaml0000644000175000017510000000034314507661043017253 0ustar nileshnilesh# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-22.04 tools: python: "3.11" python: install: - requirements: docs/requirements.txt sphinx: configuration: docs/conf.py class-registry-4.1.0/.gitignore0000644000175000017510000000221314507661043016012 0ustar nileshnilesh# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ # This directory is used by unit tests. # See `test/entry_points_test.py` for more info. !test/dummy_package.egg-info .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject