extendable 1.2.2__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. extendable-1.4.0/.gitignore +129 -0
  2. extendable-1.4.0/.pre-commit-config.yaml +18 -0
  3. extendable-1.4.0/HISTORY.rst +135 -0
  4. {extendable-1.2.2 → extendable-1.4.0}/PKG-INFO +48 -26
  5. {extendable-1.2.2 → extendable-1.4.0}/README.md +15 -8
  6. extendable-1.4.0/news/.gitignore +1 -0
  7. {extendable-1.2.2 → extendable-1.4.0}/pyproject.toml +35 -19
  8. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/__init__.py +4 -1
  9. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/main.py +54 -28
  10. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/registry.py +45 -24
  11. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/utils.py +3 -2
  12. extendable-1.4.0/tests/__init__.py +0 -0
  13. extendable-1.4.0/tests/conftest.py +32 -0
  14. extendable-1.4.0/tests/mod_base/__init__.py +1 -0
  15. extendable-1.4.0/tests/mod_base/base.py +6 -0
  16. extendable-1.4.0/tests/mod_ext1/__init__.py +1 -0
  17. extendable-1.4.0/tests/mod_ext1/base.py +7 -0
  18. extendable-1.4.0/tests/mod_ext2/__init__.py +1 -0
  19. extendable-1.4.0/tests/mod_ext2/base.py +7 -0
  20. extendable-1.4.0/tests/mod_mixin/__init__.py +0 -0
  21. extendable-1.4.0/tests/mod_mixin/a.py +5 -0
  22. extendable-1.4.0/tests/mod_mixin/b.py +6 -0
  23. extendable-1.4.0/tests/mod_mixin/m.py +5 -0
  24. extendable-1.4.0/tests/mod_mixin/mext.py +6 -0
  25. extendable-1.4.0/tests/test_registry_init.py +152 -0
  26. extendable-1.4.0/tests/test_simple.py +348 -0
  27. extendable-1.4.0/tox.ini +53 -0
  28. extendable-1.2.2/src/extendable/version.py +0 -1
  29. {extendable-1.2.2 → extendable-1.4.0}/LICENSE +0 -0
  30. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/context.py +0 -0
  31. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/exceptions.py +0 -0
  32. {extendable-1.2.2 → extendable-1.4.0}/src/extendable/py.typed +0 -0
@@ -0,0 +1,129 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
@@ -0,0 +1,18 @@
1
+ default_language_version:
2
+ python: python3
3
+ repos:
4
+ - repo: https://github.com/astral-sh/ruff-pre-commit
5
+ rev: v0.17.0
6
+ hooks:
7
+ - id: ruff-check
8
+ args: ["--fix"]
9
+ - id: ruff-format
10
+ - repo: https://github.com/pre-commit/pre-commit-hooks
11
+ rev: v6.0.0
12
+ hooks:
13
+ - id: check-merge-conflict
14
+ - id: check-toml
15
+ - id: check-yaml
16
+ - id: debug-statements
17
+ - id: end-of-file-fixer
18
+ - id: trailing-whitespace
@@ -0,0 +1,135 @@
1
+ 1.4.0 (2026-10-22)
2
+ ==================
3
+
4
+ Bugfixes
5
+ --------
6
+
7
+ - Each class is built once during registry initialization. Previously, if a class had to wait for one of its bases, all classes were rebuilt on each pass, potentially leaving the waiting class based on a class that was later replaced in the registry
8
+ - The classes built by the registry now keep the qualified name of the original
9
+ class instead of a generated one (``MyClass0``, ``MyClass1``, ...). Since these
10
+ classes are the same class for the end user, error messages and schemas
11
+ generated by third party libraries (e.g. pydantic) now use the expected name.
12
+
13
+ Also add a custom repr for aggregated classes to make them easier to
14
+ distinguish.
15
+
16
+
17
+ Deprecations and Removals
18
+ -------------------------
19
+
20
+ - Drop support for python < 3.10 (python 3.9 and older versions have reached
21
+ their end of life). Add support for python 3.14.
22
+
23
+
24
+ 1.3.0 (2025-04-16)
25
+ ==================
26
+
27
+ Bugfixes
28
+ --------
29
+
30
+ - When checking if an instance of extendable class is a type of itself, first check if both classes have the same __xreg_name__ attribute. (`#18 <https://github.com/lmignon/extendable/issues/18>`_)
31
+
32
+
33
+ 1.2.2 (2023-10-03)
34
+ ==================
35
+
36
+ Bugfixes
37
+ --------
38
+
39
+ - Fix error when a class is extended to add others aggregated classes as parent
40
+ classes. Before this fix, the builder used the list of all base classes of
41
+ the final class as base classes for every class into the hierarchy. (`#15 <https://github.com/lmignon/extendable/issues/15>`_)
42
+
43
+
44
+ 1.2.1 (2023-09-19)
45
+ ==================
46
+
47
+ Bugfixes
48
+ --------
49
+
50
+ - Fix isinstance and issublcass method on multi levels inheritance hierarchy. (`#14 <https://github.com/lmignon/extendable/pull/14>`_)
51
+
52
+
53
+ 1.2.0 (2023-07-28)
54
+ ==================
55
+
56
+ Features
57
+ --------
58
+
59
+ - Add a `before_init_registry` hook method into the `ExtendableRegistryListener` class.
60
+ This methods allows you to udpate the list of modules to load into the registry if
61
+ you need to do so. (`#before_hook <https://github.com/lmignon/extendable/pull/13>`_)
62
+
63
+
64
+ 1.1.0 (2023-07-15)
65
+ ==================
66
+
67
+ Features
68
+ --------
69
+
70
+ - Simpler syntax for defining a class extending another class. The `extends` parameter now accepts `True` as its value. `class PersonExt(Person, extends=True)` means that `PersonExt` extends its first base class: `Person`. This is equivalent to `class PersonExt(Person, extends=Person)`. (`#2 <https://github.com/lmignon/extendable/issues/2>`_)
71
+
72
+
73
+ 1.0.0 (2023-07-14)
74
+ ==================
75
+
76
+ Features
77
+ --------
78
+
79
+ - Access to the context variable used to store the current extended Classes
80
+ returns None if no context is available. Previously the access to the context
81
+ throws an exception if no context was available.
82
+ - Calls to the classmethod "_get_assembled_cls" now raises RegistryNotInitializedError
83
+ if the registry is not initialized.
84
+ - The metadaclass now provides the method `_wrap_class_method`. This method
85
+ can be used to wrap class methods in a way that when the method is called
86
+ the logic is delegated to the aggregated class if it exists.
87
+
88
+
89
+ 0.0.5 (2023-07-13)
90
+ ==================
91
+
92
+ Bugfixes
93
+ --------
94
+
95
+ - Preserve kwargs values in class defintion to ensure to properly create a concrete class with the same attributes as the original one.
96
+
97
+
98
+ 0.0.4 (2023-06-26)
99
+ ==================
100
+
101
+ Bugfixes
102
+ --------
103
+
104
+ - Fix registry rebuild (`Details <https://github.com/lmignon/extendable/pull/8/commits/120c1b749081f48893ca74d711091621c3c3481e>`_)
105
+ - Add missing type annotation
106
+
107
+
108
+ Deprecations and Removals
109
+ -------------------------
110
+
111
+ - Drop support fo py3.6
112
+
113
+
114
+ 0.0.3 (2022-11-01)
115
+ ==================
116
+
117
+ Bugfixes
118
+ --------
119
+
120
+ - Fix error when '__qualname__' is not provided (`#6 <https://github.com/lmignon/extendable/issues/6>`_)
121
+
122
+
123
+ 0.0.2 (2022-06-15)
124
+ ==================
125
+
126
+ Bugfixes
127
+ --------
128
+
129
+ - Fix compatibility issue with py3.11. (`#4 <https://github.com/lmignon/extendable/issues/4>`_)
130
+
131
+
132
+ v0.0.1 (2021-30-12)
133
+ ===================
134
+
135
+ First release.
@@ -1,27 +1,43 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.5
2
2
  Name: extendable
3
- Version: 1.2.2
3
+ Version: 1.4.0
4
4
  Summary: A lib to define class extendable at runtime.
5
+ Project-URL: Source, https://github.com/lmignon/extendable
6
+ Project-URL: Changelog, https://github.com/lmignon/extendable/blob/master/HISTORY.rst
5
7
  Author-email: Laurent Mignon <laurent.mignon@acsone.eu>
6
- Requires-Python: >=3.6
7
- Description-Content-Type: text/markdown
8
- Classifier: License :: OSI Approved :: MIT License
8
+ License: The MIT License (MIT)
9
+
10
+ Copyright (c) 2021 Laurent Mignon
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in
20
+ all copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ THE SOFTWARE.
29
+ License-File: LICENSE
9
30
  Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
10
32
  Classifier: Operating System :: OS Independent
11
- Requires-Dist: contextvars;python_version<'3.7'
12
- Requires-Dist: types-contextvars;python_version<'3.7'
13
- Requires-Dist: typing_extensions;python_version<'3.7'
14
- Requires-Dist: mypy ; extra == "mypy"
15
- Requires-Dist: towncrier ; extra == "release"
16
- Requires-Dist: bumpversion ; extra == "release"
17
- Requires-Dist: pytest ; extra == "test"
18
- Requires-Dist: pytest-mock ; extra == "test"
19
- Requires-Dist: coverage[toml] ; extra == "test"
20
- Project-URL: Changelog, https://github.com/lmignon/extendable/blob/master/HISTORY.rst
21
- Project-URL: Source, https://github.com/lmignon/extendable
33
+ Requires-Python: >=3.10
22
34
  Provides-Extra: mypy
23
- Provides-Extra: release
35
+ Requires-Dist: mypy; extra == 'mypy'
24
36
  Provides-Extra: test
37
+ Requires-Dist: coverage[toml]; extra == 'test'
38
+ Requires-Dist: pytest; extra == 'test'
39
+ Requires-Dist: pytest-mock; extra == 'test'
40
+ Description-Content-Type: text/markdown
25
41
 
26
42
  [![CI](https://github.com/lmignon/extendable/actions/workflows/ci.yml/badge.svg)](https://github.com/lmignon/extendable/actions/workflows/ci.yml)
27
43
  [![codecov](https://codecov.io/gh/lmignon/extendable/branch/master/graph/badge.svg?token=LXD34T420H)](https://codecov.io/gh/lmignon/extendable)
@@ -188,22 +204,28 @@ method into the metaclass.
188
204
  To run tests, use `tox`. You will get a test coverage report in `htmlcov/index.html`.
189
205
  An easy way to install tox is `pipx install tox`.
190
206
 
191
- This project uses pre-commit to enforce linting (among which black for code formating,
192
- isort for sorting imports, and mypy for type checking).
207
+ This project uses pre-commit to enforce linting and formatting (ruff). The type
208
+ checking (mypy) is run by `tox -e typing`.
193
209
 
194
210
  To make sure linters run locally on each of your commits, install pre-commit
195
211
  (`pipx install pre-commit` is recommended), and run `pre-commit install` in your
196
212
  local clone of the extendable repository.
197
213
 
198
- To release:
214
+ ## Release
199
215
 
200
- * run ``bumpversion patch|minor|major` --list`
201
- * Check the `new_version` value returned by the previous command
202
- * run `towncrier build`.
203
- * Inspect and commit the updated HISTORY.rst.
204
- * `git tag {new_version} ; git push --tags`.
216
+ The version of the package is computed from the git tags (hatch-vcs) and the
217
+ changelog is built from the news fragments (towncrier).
218
+
219
+ * Each change worth mentioning into the changelog comes with a news fragment
220
+ into the `news` directory: `news/<issue number>.<type>`, or
221
+ `news/+<name>.<type>` for a change without issue, where `<type>` is one of
222
+ `feature`, `bugfix`, `doc`, `removal` or `misc`.
223
+ * To release a new version, run the `release` workflow from the GitHub "Actions"
224
+ tab and choose the part of the version to increase (`patch`, `minor` or
225
+ `major`). Once the run is approved (required reviewers of the
226
+ `release-approval` environment), the workflow updates `HISTORY.rst`, tags the
227
+ release, publishes the package to PyPI and creates the GitHub release.
205
228
 
206
229
  ## Contributing
207
230
 
208
231
  All kind of contributions are welcome.
209
-
@@ -163,20 +163,27 @@ method into the metaclass.
163
163
  To run tests, use `tox`. You will get a test coverage report in `htmlcov/index.html`.
164
164
  An easy way to install tox is `pipx install tox`.
165
165
 
166
- This project uses pre-commit to enforce linting (among which black for code formating,
167
- isort for sorting imports, and mypy for type checking).
166
+ This project uses pre-commit to enforce linting and formatting (ruff). The type
167
+ checking (mypy) is run by `tox -e typing`.
168
168
 
169
169
  To make sure linters run locally on each of your commits, install pre-commit
170
170
  (`pipx install pre-commit` is recommended), and run `pre-commit install` in your
171
171
  local clone of the extendable repository.
172
172
 
173
- To release:
173
+ ## Release
174
174
 
175
- * run ``bumpversion patch|minor|major` --list`
176
- * Check the `new_version` value returned by the previous command
177
- * run `towncrier build`.
178
- * Inspect and commit the updated HISTORY.rst.
179
- * `git tag {new_version} ; git push --tags`.
175
+ The version of the package is computed from the git tags (hatch-vcs) and the
176
+ changelog is built from the news fragments (towncrier).
177
+
178
+ * Each change worth mentioning into the changelog comes with a news fragment
179
+ into the `news` directory: `news/<issue number>.<type>`, or
180
+ `news/+<name>.<type>` for a change without issue, where `<type>` is one of
181
+ `feature`, `bugfix`, `doc`, `removal` or `misc`.
182
+ * To release a new version, run the `release` workflow from the GitHub "Actions"
183
+ tab and choose the part of the version to increase (`patch`, `minor` or
184
+ `major`). Once the run is approved (required reviewers of the
185
+ `release-approval` environment), the workflow updates `HISTORY.rst`, tags the
186
+ release, publishes the package to PyPI and creates the GitHub release.
180
187
 
181
188
  ## Contributing
182
189
 
@@ -0,0 +1 @@
1
+ !.gitignore
@@ -1,9 +1,10 @@
1
1
  [build-system]
2
- requires = ["flit_core >=3.2,<4"]
3
- build-backend = "flit_core.buildapi"
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "extendable"
7
+ description = "A lib to define class extendable at runtime."
7
8
  authors = [{name = "Laurent Mignon", email = "laurent.mignon@acsone.eu"}]
8
9
  readme = "README.md"
9
10
  license = {file = "LICENSE"}
@@ -12,13 +13,9 @@ classifiers = [
12
13
  "Intended Audience :: Developers",
13
14
  "Operating System :: OS Independent",
14
15
  ]
15
- dynamic = ["version", "description"]
16
- dependencies = [
17
- "contextvars;python_version<'3.7'",
18
- "types-contextvars;python_version<'3.7'",
19
- "typing_extensions;python_version<'3.7'",
20
- ]
21
- requires-python = ">=3.6"
16
+ dynamic = ["version"]
17
+ dependencies = []
18
+ requires-python = ">=3.10"
22
19
 
23
20
  [project.optional-dependencies]
24
21
  test = [
@@ -29,21 +26,41 @@ test = [
29
26
  mypy = [
30
27
  "mypy",
31
28
  ]
32
- release = [
33
- "towncrier",
34
- "bumpversion",
35
- ]
36
29
 
37
30
  [project.urls]
38
31
  Source = "https://github.com/lmignon/extendable"
39
32
  Changelog = "https://github.com/lmignon/extendable/blob/master/HISTORY.rst"
40
33
 
41
- [tool.flit.sdist]
34
+ [tool.hatch.version]
35
+ # the version is computed from the git tags
36
+ source = "vcs"
37
+
38
+ [tool.hatch.build.targets.sdist]
42
39
  exclude = [".github"]
43
40
 
44
- [tool.isort]
45
- profile = "black"
46
- multi_line_output = 3
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/extendable"]
43
+
44
+ [tool.ruff]
45
+ # keep the examples of the documentation as written (e.g. the "#>" outputs)
46
+ extend-exclude = ["*.md"]
47
+
48
+ [tool.ruff.lint]
49
+ select = [
50
+ "E", # pycodestyle errors
51
+ "W", # pycodestyle warnings
52
+ "F", # pyflakes
53
+ "I", # isort
54
+ "B", # flake8-bugbear
55
+ "C4", # flake8-comprehensions
56
+ "UP", # pyupgrade
57
+ ]
58
+ ignore = [
59
+ "E501", # line too long, handled by the formatter
60
+ ]
61
+
62
+ [tool.ruff.lint.per-file-ignores]
63
+ "__init__.py" = ["F401"]
47
64
 
48
65
  [tool.coverage.run]
49
66
  branch = true
@@ -59,8 +76,7 @@ exclude_lines = [
59
76
  ]
60
77
 
61
78
  [tool.towncrier]
62
- package = "extendable"
63
- package_dir = "src"
79
+ name = "Extendable"
64
80
  filename = "HISTORY.rst"
65
81
  directory = "news"
66
82
  issue_format = "`#{issue} <https://github.com/lmignon/extendable/issues/{issue}>`_"
@@ -1,8 +1,11 @@
1
1
  """A lib to define class extendable at runtime."""
2
2
 
3
+ from importlib.metadata import version
4
+
3
5
  # shortcut to main used class
4
6
  from .main import ExtendableMeta
5
- from .version import __version__
7
+
8
+ __version__ = version("extendable")
6
9
 
7
10
  # declare "public" members
8
11
  # __all__ doesn't restrict access to others members, but they are at least
@@ -1,15 +1,14 @@
1
1
  import collections
2
2
  import functools
3
3
  import inspect
4
- import sys
5
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Type, no_type_check
6
-
7
- if sys.version_info >= (3, 7):
8
- from typing import OrderedDict
9
- else:
10
- from typing_extensions import OrderedDict
11
-
12
4
  from abc import ABCMeta
5
+ from collections import OrderedDict
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Optional,
10
+ no_type_check,
11
+ )
13
12
 
14
13
  from .context import extendable_registry
15
14
  from .exceptions import RegistryNotInitializedError
@@ -23,23 +22,23 @@ if TYPE_CHECKING:
23
22
 
24
23
  class ExtendableClassDef:
25
24
  name: str
26
- base_names: List[str]
27
- original_base_names: List[str]
28
- namespace: Dict[str, Any]
25
+ base_names: list[str]
26
+ original_base_names: list[str]
27
+ namespace: dict[str, Any]
29
28
  original_name: str
30
- others_bases: List[Any]
31
- hierarchy: List["ExtendableClassDef"]
29
+ others_bases: list[Any]
30
+ hierarchy: list["ExtendableClassDef"]
32
31
  metaclass: "ExtendableMeta"
33
- original_cls: Type["ExtendableMeta"]
34
- kwargs: Dict[str, Any]
32
+ original_cls: type["ExtendableMeta"]
33
+ kwargs: dict[str, Any]
35
34
 
36
35
  def __init__(
37
36
  self,
38
37
  original_name: str,
39
- bases: List[Any],
40
- namespace: Dict[str, Any],
38
+ bases: list[Any],
39
+ namespace: dict[str, Any],
41
40
  metaclass: "ExtendableMeta",
42
- kwargs: Dict[str, Any],
41
+ kwargs: dict[str, Any],
43
42
  ) -> None:
44
43
  self.namespace = namespace
45
44
  self.name = namespace["__xreg_name__"]
@@ -87,28 +86,27 @@ class ExtendableClassDef:
87
86
  return clone
88
87
 
89
88
 
90
- _extendable_class_defs_by_module: OrderedDict[
91
- str, List[ExtendableClassDef]
92
- ] = collections.OrderedDict()
89
+ _extendable_class_defs_by_module: OrderedDict[str, list[ExtendableClassDef]] = (
90
+ collections.OrderedDict()
91
+ )
93
92
 
94
93
 
95
94
  def __register_class_def__(module: str, cls_def: ExtendableClassDef) -> None:
96
- global _extendable_class_defs_by_module
97
95
  if module not in _extendable_class_defs_by_module:
98
96
  _extendable_class_defs_by_module[module] = []
99
97
  _extendable_class_defs_by_module[module].append(cls_def)
100
98
 
101
99
 
102
100
  class ExtendableMeta(ABCMeta):
103
- __xreg_base_names__: List[str]
101
+ __xreg_base_names__: list[str]
104
102
  __xreg_name__: str
105
- __xreg_all_base_names__: Set[str]
103
+ __xreg_all_base_names__: set[str]
106
104
  _is_aggregated_class: bool
107
105
  _original_cls: "ExtendableMeta"
108
106
 
109
107
  @no_type_check
110
108
  def __new__(metacls, name, bases, namespace, extends=None, **kwargs):
111
- """create the expected class and collect the class definition that will be used
109
+ """Create the expected class and collect the class definition that will be used
112
110
  at the end of registry load process to build the final class."""
113
111
  class_def = None
114
112
  if isinstance(extends, bool) and extends:
@@ -191,10 +189,10 @@ class ExtendableMeta(ABCMeta):
191
189
  return super().__new__(metacls, name, bases, namespace, **kwargs)
192
190
 
193
191
  @classmethod
194
- def _wrap_class_methods(metacls, namespace: Dict[str, Any]) -> Dict[str, Any]:
192
+ def _wrap_class_methods(metacls, namespace: dict[str, Any]) -> dict[str, Any]:
195
193
  """Wrap classmethods defined into the namespace to delegate the call to the
196
194
  final class."""
197
- new_namespace: Dict[str, Any] = {}
195
+ new_namespace: dict[str, Any] = {}
198
196
  for key, value in namespace.items():
199
197
  if isinstance(value, classmethod):
200
198
  new_namespace[key] = metacls._wrap_class_method(value, key)
@@ -203,7 +201,7 @@ class ExtendableMeta(ABCMeta):
203
201
  return new_namespace
204
202
 
205
203
  @classmethod
206
- def _is_extendable(metacls, cls: Type[Any]) -> bool:
204
+ def _is_extendable(metacls, cls: type[Any]) -> bool:
207
205
  return issubclass(type(cls), ExtendableMeta)
208
206
 
209
207
  @classmethod
@@ -248,9 +246,37 @@ class ExtendableMeta(ABCMeta):
248
246
  return super().__call__(*args, **kwargs)
249
247
  return cls._get_assembled_cls()(*args, **kwargs)
250
248
 
249
+ def __repr__(cls) -> str:
250
+ """Add to the representation of the classes built by the registry their level
251
+ into the class hierarchy and the class definition they come from.
252
+
253
+ e.g. ``<class 'addon_b.Location' #2 (addon_b.LocationCode)>``
254
+ """
255
+ level = cls.__dict__.get("__xreg_level__")
256
+ if level is None:
257
+ return super().__repr__()
258
+ name = f"{cls.__module__}.{cls.__qualname__}"
259
+ original_cls = cls.__dict__["__xreg_original_cls__"]
260
+ original_name = f"{original_cls.__module__}.{original_cls.__qualname__}"
261
+ if original_name == name:
262
+ return f"<class '{name}' #{level}>"
263
+ return f"<class '{name}' #{level} ({original_name})>"
264
+
251
265
  ###############################################################
252
266
  # concrete methods provided to the final class by the metaclass
253
267
  ###############################################################
268
+ def __instancecheck__(self, instance: Any) -> bool: # noqa: B902
269
+ """Implement issubclass(sub, cls)."""
270
+ if not hasattr(instance, "__xreg_name__"):
271
+ return False
272
+
273
+ if instance.__xreg_name__ == self.__xreg_name__:
274
+ # this is the same class
275
+ return True
276
+ # self is a class and instance is an instance of a class
277
+ if self.__xreg_name__ in instance.__xreg_all_base_names__:
278
+ return True
279
+ return super().__instancecheck__(instance)
254
280
 
255
281
  def __subclasscheck__(cls, subclass: Any) -> bool: # noqa: B902
256
282
  """Implement issubclass(sub, cls)."""