clear-skies-doc-builder 2.0.0__py3-none-any.whl

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 (37) hide show
  1. clear_skies_doc_builder-2.0.0.dist-info/LICENSE +21 -0
  2. clear_skies_doc_builder-2.0.0.dist-info/METADATA +40 -0
  3. clear_skies_doc_builder-2.0.0.dist-info/RECORD +37 -0
  4. clear_skies_doc_builder-2.0.0.dist-info/WHEEL +4 -0
  5. clearskies_doc_builder/__init__.py +34 -0
  6. clearskies_doc_builder/backends/__init__.py +9 -0
  7. clearskies_doc_builder/backends/attribute_backend.py +90 -0
  8. clearskies_doc_builder/backends/class_backend.py +98 -0
  9. clearskies_doc_builder/backends/module_backend.py +139 -0
  10. clearskies_doc_builder/backends/python.py +0 -0
  11. clearskies_doc_builder/build_callable.py +24 -0
  12. clearskies_doc_builder/builders/__init__.py +9 -0
  13. clearskies_doc_builder/builders/builder.py +136 -0
  14. clearskies_doc_builder/builders/module.py +68 -0
  15. clearskies_doc_builder/builders/single_class.py +78 -0
  16. clearskies_doc_builder/builders/single_class_to_section.py +38 -0
  17. clearskies_doc_builder/columns/__init__.py +17 -0
  18. clearskies_doc_builder/columns/any.py +38 -0
  19. clearskies_doc_builder/columns/attribute.py +34 -0
  20. clearskies_doc_builder/columns/attributes.py +34 -0
  21. clearskies_doc_builder/columns/base_classes.py +31 -0
  22. clearskies_doc_builder/columns/class_column.py +37 -0
  23. clearskies_doc_builder/columns/method.py +0 -0
  24. clearskies_doc_builder/columns/module.py +38 -0
  25. clearskies_doc_builder/columns/module_classes.py +27 -0
  26. clearskies_doc_builder/models/__init__.py +13 -0
  27. clearskies_doc_builder/models/arg.py +0 -0
  28. clearskies_doc_builder/models/attribute.py +14 -0
  29. clearskies_doc_builder/models/attribute_reference.py +7 -0
  30. clearskies_doc_builder/models/class_model.py +24 -0
  31. clearskies_doc_builder/models/class_reference.py +7 -0
  32. clearskies_doc_builder/models/method.py +19 -0
  33. clearskies_doc_builder/models/method_reference.py +7 -0
  34. clearskies_doc_builder/models/module.py +17 -0
  35. clearskies_doc_builder/models/module_reference.py +7 -0
  36. clearskies_doc_builder/models/property.py +0 -0
  37. clearskies_doc_builder/prepare_doc_space.py +22 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 clearskies-py
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: clear-skies-doc-builder
3
+ Version: 2.0.0
4
+ Summary: The docbuilder for all 'official' clearskies plugins (as well as the main clearskies docs)
5
+ License: MIT
6
+ Author: Conor Mancone
7
+ Author-email: cmancone@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: clear-skies (>=2.0.0,<3.0.0)
17
+ Project-URL: Repository, https://github.com/clearskies-py/docs
18
+ Description-Content-Type: text/markdown
19
+
20
+ # docs
21
+
22
+ The documentation builder for clearskies and related plugins
23
+
24
+ ## Overview
25
+
26
+ Each "official" clearskies module (including the core clearskies library itself) has the documentation primarily written in the codebase as docblocks. The documentation site is then built by extracting these docblocks and stitching them together. To be clear, this isn't about the low-level "API" documentation that describes every single class/method in the framework. Rather, this is about the primary documentation site itself (clearskies.info) which is focused on high-level use cases and examples of the primary configuration options. As a result, it's not a simple matter of just iterating over the classes/methods and building documentation. To build a coherent documentation site, each plugin has a configuration file that basically outlines the final "structure" or organization of the resulting documentation, as well as the name of a builder class that will combine that configuration information with the codebase itself to create the actual docs.
27
+
28
+ The docs themselves (in the source code) are all written with markdown. This documentation builder then takes that markdwon and adds the necessary headers/etc so to make them valid files for [Jekyll](https://jekyllrb.com/), the builder for the current documentation site. The site itself is hosted in S3, so building an actual documentation site means:
29
+
30
+ 1. Properly documenting everything inside of the source code via markdown.
31
+ 2. Creating a config file (`docs/python/config.json`) to map source code docs to Jekyll files.
32
+ 3. Creating a skeleton of a Jekyll site in the `doc/jekyll` folder of the plugin.
33
+ 4. Installing this doc builder via `poetry add clear-skies-doc-builder`.
34
+ 5. Run the doc builder.
35
+ 6. Build with Jekyll.
36
+ 7. Push to the appropriate subfolder via S3.
37
+ 8. (Only once) Update the main clearskies doc site to know about the new subfolder for this plugin.
38
+
39
+ Of course, we want the Jekyll sites to be consistent with eachother in terms of style/look. In the long run we'll probably have this doc builder also bootstrap the Jekyll site, but for now you just have to manually setup the Jekyll build using the main clearskies repo as a template.
40
+
@@ -0,0 +1,37 @@
1
+ clearskies_doc_builder/__init__.py,sha256=8Z9XH0MX0B1WTMwTEypOuZopePhZ5coLO6tTaVYvsmQ,1299
2
+ clearskies_doc_builder/backends/__init__.py,sha256=q5jpy8xfZ4SbGQ1T30q4mp4h346HaMivvmNqtgTMQxw,303
3
+ clearskies_doc_builder/backends/attribute_backend.py,sha256=cMj5XdMN550nUHkmc4-KPKn5PwMg3aFgQr3Hx7Cvibw,3580
4
+ clearskies_doc_builder/backends/class_backend.py,sha256=XNN3svPEMNOP81IRVWzsV206Q8U70ZE8ilVoqYq4SoU,4030
5
+ clearskies_doc_builder/backends/module_backend.py,sha256=JCJZCxD8NlWvN4VtoQNDCqLHU1PYglM5GYvsV8qTLjw,5790
6
+ clearskies_doc_builder/backends/python.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ clearskies_doc_builder/build_callable.py,sha256=9_HrytRA5MeGpuUKRFgmlK5Mb1WKt6TfWlCOi1aS-Pc,983
8
+ clearskies_doc_builder/builders/__init__.py,sha256=ndAK4-imR9vhl7CS425ASYi46FN5-eR9Dr6bKxCn5lA,292
9
+ clearskies_doc_builder/builders/builder.py,sha256=PkO6h-W-qLbQt2iUeb4c0WEir4oa_9ozJYLqOgYoXGM,6133
10
+ clearskies_doc_builder/builders/module.py,sha256=LZY2wx9UUe257usrkCUkuuyZNDa3flcgxRiTBJH8luI,3283
11
+ clearskies_doc_builder/builders/single_class.py,sha256=8KGLewKwD1-MeFVyfUjz7gWbSLTngkfTPebIO2Vu3QY,3451
12
+ clearskies_doc_builder/builders/single_class_to_section.py,sha256=kYokUx-JMR66nzJLQ_hECzptHz6ZWAQyDoM39B0uX08,1751
13
+ clearskies_doc_builder/columns/__init__.py,sha256=--cof8kFPwlZncM3MBQYkvfq8TejEeI0B99vTlEVCkU,566
14
+ clearskies_doc_builder/columns/any.py,sha256=j9WW4IRkbO6AQci8OmJUUpoFPR1y9SzbR2fR_HnSpDo,997
15
+ clearskies_doc_builder/columns/attribute.py,sha256=5QwFUZ93me1dF6_W2U-d3KOHoet9w01i-u4U5LKaa24,970
16
+ clearskies_doc_builder/columns/attributes.py,sha256=Y6ieOcPkd9orAa2OQHRD2w8GTtLFwAG3-CAqIaHb6e0,965
17
+ clearskies_doc_builder/columns/base_classes.py,sha256=GmVrAyfaxX9tT2WUYvz9kCkzHDdVxE2eSatzUYCsmRQ,893
18
+ clearskies_doc_builder/columns/class_column.py,sha256=zGFcu4ch8EhF9wglBvxHZbjbudwSWKrm22aRXsXZoAE,964
19
+ clearskies_doc_builder/columns/method.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ clearskies_doc_builder/columns/module.py,sha256=OrbxVARbwpeHa4DRJhG4ScPlicIvwlEkbp3okd9OZJQ,1006
21
+ clearskies_doc_builder/columns/module_classes.py,sha256=4CsXcku0hHVZ0V26u1wzfHYXcHgntFo9hmj91Wn5QlU,728
22
+ clearskies_doc_builder/models/__init__.py,sha256=WY9giZKKl81NNSJtLHPW6iZDu12R0r1MK3zgiYUOJ4s,402
23
+ clearskies_doc_builder/models/arg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ clearskies_doc_builder/models/attribute.py,sha256=1YpL6JGoZNu3ss3Zh_hpByFLqj8s9co8Meo_uuSqx5c,384
25
+ clearskies_doc_builder/models/attribute_reference.py,sha256=u_H7uYGgEYFAZHhPodBg6HJuTUxNkw5kG3NuEYXIjME,188
26
+ clearskies_doc_builder/models/class_model.py,sha256=jOwbp1D69i8YEUngd6-u_RAhrGTxekW4wzTTpaaNzug,897
27
+ clearskies_doc_builder/models/class_reference.py,sha256=CnbJubWq2Px0w_ECJBMXuJ3kGrG7yPzHa2U63EoDzxA,184
28
+ clearskies_doc_builder/models/method.py,sha256=ijJffYVQrRh9t5COEw9DhveihTraC1zNlTl1P_ktWqU,492
29
+ clearskies_doc_builder/models/method_reference.py,sha256=PiH7y7iLJDY_nJ0V-RrdAFHozGr5mmKbZBi-KP56bEA,176
30
+ clearskies_doc_builder/models/module.py,sha256=oDTbuCJBix3wcV_FPECp4ZtBDVbx324w3zIaLuY_aqY,571
31
+ clearskies_doc_builder/models/module_reference.py,sha256=Jz_3iXovaNs65twrFKX95l5jJpkVIi9L-w1XpjhrX7U,176
32
+ clearskies_doc_builder/models/property.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ clearskies_doc_builder/prepare_doc_space.py,sha256=i1bDcorrLTJ12os2ugupUYlRGLwoPlLnly70u-BnjoI,593
34
+ clear_skies_doc_builder-2.0.0.dist-info/LICENSE,sha256=Vfhf-H2GuXDYHeEFF3QIkgjANSbOvflU_Gx7j9WX3sI,1070
35
+ clear_skies_doc_builder-2.0.0.dist-info/METADATA,sha256=DC8RAUktXXDa16Ha5EIPKZkULRlV9fVOOLqz5uXTczo,2980
36
+ clear_skies_doc_builder-2.0.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
37
+ clear_skies_doc_builder-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,34 @@
1
+ import pathlib
2
+ import sys
3
+ import json
4
+
5
+ import clearskies
6
+ from clearskies_doc_builder import models, backends
7
+ from clearskies_doc_builder.build_callable import build_callable
8
+
9
+
10
+ def build(build_file_string: str) -> None:
11
+ # We assume a folder structure here where the repo root contains a `src/` folder and a `docs/python` folder.
12
+ # `build_file_string` should contain the absolute path to the file that kicked this off, which should
13
+ # live in the `docs/python` folder. This comes in as a string, which we convert to a path.
14
+ # We then also need to calculate the path to the `src/` folder and add that to our
15
+ # python path. We do this because we want to import the clearskies module in question, since the python
16
+ # code is where all of our documentation lives.
17
+
18
+ doc_python_path = pathlib.Path(build_file_string).parents[0]
19
+ project_root = doc_python_path.parents[1]
20
+ sys.path.append(str(project_root / "src"))
21
+
22
+ config_file = open(str(doc_python_path / "config.json"), "r")
23
+ config = json.loads(config_file.read())
24
+ config_file.close()
25
+
26
+ cli = clearskies.contexts.Cli(
27
+ build_callable,
28
+ modules=[models, backends],
29
+ bindings={
30
+ "config": config,
31
+ "project_root": project_root / "docs",
32
+ },
33
+ )
34
+ cli()
@@ -0,0 +1,9 @@
1
+ from clearskies_doc_builder.backends.attribute_backend import AttributeBackend
2
+ from clearskies_doc_builder.backends.class_backend import ClassBackend
3
+ from clearskies_doc_builder.backends.module_backend import ModuleBackend
4
+
5
+ __all__ = [
6
+ "AttributeBackend",
7
+ "ClassBackend",
8
+ "ModuleBackend",
9
+ ]
@@ -0,0 +1,90 @@
1
+ import inspect
2
+ from typing import Any
3
+
4
+ import clearskies
5
+ import clearskies.model
6
+ import clearskies.column
7
+ import clearskies.query
8
+ from clearskies_doc_builder.backends.module_backend import ModuleBackend
9
+
10
+
11
+ class AttributeBackend(ModuleBackend):
12
+ _search_functions = {
13
+ "id": lambda attribute, name, value: id(attribute) == int(value), # type: ignore
14
+ "name": lambda attribute, name, value: name == value, # type: ignore
15
+ "type": lambda attribute, name, value: attribute.__class__ == value, # type: ignore
16
+ }
17
+
18
+ def records(
19
+ self, query: clearskies.query.Query, next_page_data: dict[str, str | int] | None = None
20
+ ) -> list[dict[str, Any]]:
21
+ """
22
+ Returns a list of records that match the given query configuration
23
+
24
+ next_page_data is used to return data to the caller. Pass in an empty dictionary, and it will be populated
25
+ with the data needed to return the next page of results. If it is still an empty dictionary when returned,
26
+ then there is no additional data.
27
+ """
28
+ disallowed = ["joins", "selects", "group_by"]
29
+ for attribute_name in disallowed:
30
+ if getattr(query, attribute_name):
31
+ raise ValueError(f"The AttributeBackend received {attribute_name} in a query but doesn't support this.")
32
+
33
+ for condition in query.conditions:
34
+ if condition.operator != "=":
35
+ raise ValueError("The AttributeBackend only supports searching with the equals operator")
36
+
37
+ if "parent_class" not in query.conditions_by_column:
38
+ raise ValueError("When searching for attributes you must include a condition on 'parent_class'")
39
+
40
+ parent_class = query.conditions_by_column["parent_class"][0].values[0]
41
+ matching_attributes = []
42
+ for name in dir(parent_class):
43
+ attribute = getattr(parent_class, name)
44
+ matches = True
45
+ for condition in query.conditions:
46
+ if condition.column_name not in self._search_functions:
47
+ continue
48
+ if not self._search_functions[condition.column_name](attribute, name, condition.values[0]): # type: ignore
49
+ matches = False
50
+
51
+ if not matches:
52
+ continue
53
+ matching_attributes.append(self.unpack(attribute, name, parent_class))
54
+
55
+ return self.paginate(matching_attributes, query)
56
+
57
+ def unpack(self, attribute: Any, name: str, parent_class: type) -> dict[str, Any]: # type: ignore
58
+ all_args = []
59
+ args = []
60
+ kwargs = []
61
+ defaults: dict[str, Any] = {}
62
+ argdata = None
63
+ try:
64
+ argdata = inspect.getfullargspec(attribute)
65
+ except:
66
+ pass
67
+
68
+ if argdata:
69
+ nargs = len(argdata.args)
70
+ nkwargs = len(argdata.defaults) if argdata.defaults else 0
71
+ npargs = nargs - nkwargs
72
+ all_args = argdata.args
73
+ kwargs = all_args[nargs - nkwargs :]
74
+ args = all_args[:nkwargs]
75
+ defaults = {}
76
+ if argdata.defaults:
77
+ defaults = {argdata.args[index + npargs]: default for (index, default) in enumerate(argdata.defaults)}
78
+
79
+ return {
80
+ "id": id(attribute),
81
+ "name": name,
82
+ "type": attribute.__class__,
83
+ "doc": attribute.__doc__,
84
+ "parent_class": parent_class,
85
+ "attribute": attribute,
86
+ "all_args": all_args,
87
+ "args": args,
88
+ "kwargs": kwargs,
89
+ "defaults": defaults,
90
+ }
@@ -0,0 +1,98 @@
1
+ from typing import Any
2
+ from types import ModuleType
3
+ import importlib
4
+ import inspect
5
+
6
+ import clearskies
7
+ import clearskies.model
8
+ import clearskies.column
9
+ import clearskies.query
10
+ from clearskies_doc_builder.backends.module_backend import ModuleBackend
11
+
12
+
13
+ class ClassBackend(ModuleBackend):
14
+ _search_functions = {
15
+ "id": lambda module, value: id(module) == int(value),
16
+ "source_file": lambda module, value: (module.__file__ if hasattr(module, "__file__") else "") == value,
17
+ }
18
+
19
+ def records(
20
+ self, query: clearskies.query.Query, next_page_data: dict[str, str | int] | None = None
21
+ ) -> list[dict[str, Any]]:
22
+ """
23
+ Returns a list of records that match the given query configuration
24
+
25
+ next_page_data is used to return data to the caller. Pass in an empty dictionary, and it will be populated
26
+ with the data needed to return the next page of results. If it is still an empty dictionary when returned,
27
+ then there is no additional data.
28
+ """
29
+ disallowed = ["joins", "selects", "group_by"]
30
+ for attribute_name in disallowed:
31
+ if getattr(query, attribute_name):
32
+ raise ValueError(f"The ClassBackend received {attribute_name} in a query but doesn't support this.")
33
+
34
+ for condition in query.conditions:
35
+ if condition.operator != "=":
36
+ raise ValueError("The ClassBackend only supports searching with the equals operator")
37
+
38
+ if "import_path" in query.conditions_by_column:
39
+ import_path = query.conditions_by_column["import_path"][0].values[0]
40
+ path_parts = import_path.split(".")
41
+ if len(path_parts) < 2:
42
+ raise ValueError(
43
+ 'In order to search for classes by import path you must provide the module and class name, e.g. `classes.find("import_path=clearskies.Endpoint")`'
44
+ )
45
+ class_name = path_parts[-1]
46
+ module_path = ".".join(path_parts[0:-1])
47
+ module = importlib.import_module(module_path)
48
+ if not hasattr(module, class_name):
49
+ raise ValueError(f"Module {import_path} has no class named {class_name}")
50
+ Class = getattr(module, class_name)
51
+ if not inspect.isclass(Class):
52
+ raise ValueError(
53
+ f"I was asked to import the class named '{import_path}' but this doesn't actually reference a class"
54
+ )
55
+ return [self.unpack(Class, module)]
56
+
57
+ if "module" not in query.conditions_by_column:
58
+ raise ValueError(
59
+ "When searching for classes you must include a condition on either 'module' or 'import_path'"
60
+ )
61
+
62
+ parent_module = query.conditions_by_column["module"][0].values[0]
63
+ matching_classes = []
64
+ for name in dir(parent_module):
65
+ attribute = getattr(parent_module, name)
66
+ if not inspect.isclass(attribute):
67
+ continue
68
+
69
+ matches = True
70
+ for condition in query.conditions:
71
+ if condition.column_name not in self._search_functions:
72
+ continue
73
+ if not self._search_functions[condition.column_name](attribute, condition.values[0]):
74
+ matches = False
75
+
76
+ if not matches:
77
+ continue
78
+ matching_classes.append(self.unpack(attribute, parent_module))
79
+
80
+ return self.paginate(matching_classes, query)
81
+
82
+ def unpack(self, Class: type, module: ModuleType) -> dict[str, Any]: # type: ignore
83
+ source_file = ""
84
+ try:
85
+ # this fails for built ins
86
+ source_file = inspect.getfile(Class)
87
+ except TypeError:
88
+ pass
89
+
90
+ return {
91
+ "id": id(Class),
92
+ "import_path": Class.__module__ + "." + Class.__name__,
93
+ "name": Class.__name__,
94
+ "source_file": source_file,
95
+ "doc": Class.__doc__,
96
+ "module": module,
97
+ "type": Class,
98
+ }
@@ -0,0 +1,139 @@
1
+ from typing import Any, Callable
2
+ from types import ModuleType
3
+ import sys
4
+ import importlib
5
+
6
+ import clearskies
7
+ import clearskies.model
8
+ import clearskies.column
9
+ import clearskies.query
10
+ from clearskies.autodoc.schema import Schema as AutoDocSchema
11
+ from clearskies.autodoc.schema import Integer as AutoDocInteger
12
+
13
+
14
+ class ModuleBackend(clearskies.backends.Backend):
15
+ _search_functions = {
16
+ "id": lambda module, value: id(module) == int(value),
17
+ "is_builtin": lambda module, value: (0 if hasattr(module, "__file__") else 1) == int(value),
18
+ "source_file": lambda module, value: (module.__file__ if hasattr(module, "__file__") else "") == value,
19
+ "module": lambda module, value: id(module) == id(value),
20
+ }
21
+
22
+ def update(self, id: int | str, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
23
+ """
24
+ Updates the record with the given id with the information from the data dictionary
25
+ """
26
+ raise Exception(f"The {self.__class__.__name__} only supports read operations: update is not allowed")
27
+
28
+ def create(self, data: dict[str, Any], model: clearskies.model.Model) -> dict[str, Any]:
29
+ """
30
+ Creates a record with the information from the data dictionary
31
+ """
32
+ raise Exception(f"The {self.__class__.__name__} only supports read operations: create is not allowed")
33
+
34
+ def delete(self, id: int | str, model: clearskies.model.Model) -> bool:
35
+ """
36
+ Deletes the record with the given id
37
+ """
38
+ raise Exception(f"The {self.__class__.__name__} only supports read operations: delete is not allowed")
39
+
40
+ def count(self, query: clearskies.query.Query) -> int:
41
+ """
42
+ Returns the number of records which match the given query configuration
43
+ """
44
+ return len(self.records(query))
45
+
46
+ def records(
47
+ self, query: clearskies.query.Query, next_page_data: dict[str, str | int] | None = None
48
+ ) -> list[dict[str, Any]]:
49
+ """
50
+ Returns a list of records that match the given query configuration
51
+
52
+ next_page_data is used to return data to the caller. Pass in an empty dictionary, and it will be populated
53
+ with the data needed to return the next page of results. If it is still an empty dictionary when returned,
54
+ then there is no additional data.
55
+ """
56
+ disallowed = ["joins", "selects", "group_by"]
57
+ for attribute_name in disallowed:
58
+ if getattr(query, attribute_name):
59
+ raise ValueError(f"The ModuleBackend received {attribute_name} in a query but doesn't support this.")
60
+
61
+ for condition in query.conditions:
62
+ if condition.operator != "=":
63
+ raise ValueError("The ModuleBackend only supports searching with the equals operator")
64
+
65
+ module_name_condition = query.conditions_by_column.get("import_path", query.conditions_by_column.get("name"))
66
+ if module_name_condition:
67
+ module_name = module_name_condition[0].values[0]
68
+ module = importlib.import_module(module_name)
69
+ return [self.unpack(module)]
70
+
71
+ matching_modules = []
72
+ module_names = set(sys.modules) & set(globals())
73
+ for module_name in module_names:
74
+ if module_name not in sys.modules:
75
+ continue
76
+
77
+ module = sys.modules[module_name]
78
+ matches = True
79
+ for condition in query.conditions:
80
+ if condition.column_name not in self._search_functions:
81
+ continue
82
+ if not self._search_functions[condition.column_name](module, condition.values[0]):
83
+ matches = False
84
+
85
+ if not matches:
86
+ continue
87
+
88
+ matching_modules.append(self.unpack(module))
89
+
90
+ return self.paginate(matching_modules, query)
91
+
92
+ def unpack(self, module: ModuleType) -> dict[str, Any]:
93
+ return {
94
+ "id": id(module),
95
+ "import_path": module.__name__,
96
+ "name": module.__name__,
97
+ "is_builtin": not hasattr(module, "__file__"),
98
+ "source_file": module.__file__ if hasattr(module, "__file__") else "",
99
+ "doc": module.__doc__,
100
+ "module": module,
101
+ }
102
+
103
+ def paginate(self, records, query):
104
+ return records
105
+
106
+ def validate_pagination_data(self, data: dict[str, Any], case_mapping: Callable) -> str:
107
+ extra_keys = set(data.keys()) - set(self.allowed_pagination_keys())
108
+ if len(extra_keys):
109
+ key_name = case_mapping("start")
110
+ return "Invalid pagination key(s): '" + "','".join(extra_keys) + f"'. Only '{key_name}' is allowed"
111
+ if "start" not in data:
112
+ key_name = case_mapping("start")
113
+ return f"You must specify '{key_name}' when setting pagination"
114
+ start = data["start"]
115
+ try:
116
+ start = int(start)
117
+ except:
118
+ key_name = case_mapping("start")
119
+ return f"Invalid pagination data: '{key_name}' must be a number"
120
+ return ""
121
+
122
+ def allowed_pagination_keys(self) -> list[str]:
123
+ return ["start"]
124
+
125
+ def documentation_pagination_next_page_response(self, case_mapping: Callable[[str], str]) -> list[Any]:
126
+ return [AutoDocInteger(case_mapping("start"), example=0)]
127
+
128
+ def documentation_pagination_next_page_example(self, case_mapping: Callable[[str], str]) -> dict[str, Any]:
129
+ return {case_mapping("start"): 0}
130
+
131
+ def documentation_pagination_parameters(
132
+ self, case_mapping: Callable[[str], str]
133
+ ) -> list[tuple[AutoDocSchema, str]]:
134
+ return [
135
+ (
136
+ AutoDocInteger(case_mapping("start"), example=0),
137
+ "The zero-indexed record number to start listing results from",
138
+ )
139
+ ]
File without changes
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ from clearskies_doc_builder import models
4
+ from clearskies_doc_builder.prepare_doc_space import prepare_doc_space
5
+
6
+
7
+ def build_callable(modules: models.Module, classes: models.Class, config: dict[str, Any], project_root: str):
8
+ doc_root = prepare_doc_space(project_root)
9
+ nav_order_parent_count = {}
10
+
11
+ for index, branch in enumerate(config["tree"]):
12
+ nav_order_title_tracker = branch.get("parent", branch["title"])
13
+ if nav_order_title_tracker not in nav_order_parent_count:
14
+ nav_order_parent_count[nav_order_title_tracker] = 0
15
+ nav_order_parent_count[nav_order_title_tracker] += 1
16
+ builder_class = classes.find("import_path=" + branch["builder"]).type
17
+ builder = builder_class(
18
+ branch,
19
+ modules,
20
+ classes,
21
+ doc_root,
22
+ nav_order=nav_order_parent_count[nav_order_title_tracker] if branch.get("parent") else index + 2,
23
+ )
24
+ builder.build()
@@ -0,0 +1,9 @@
1
+ from clearskies_doc_builder.builders.module import Module
2
+ from clearskies_doc_builder.builders.single_class import SingleClass
3
+ from clearskies_doc_builder.builders.single_class_to_section import SingleClassToSection
4
+
5
+ __all__ = [
6
+ "Module",
7
+ "SingleClass",
8
+ "SingleClassToSection",
9
+ ]
@@ -0,0 +1,136 @@
1
+ import re
2
+ import tokenize
3
+ import pathlib
4
+
5
+
6
+ class Builder:
7
+ _attribute_cache: dict[str, dict[str, str]] = {}
8
+
9
+ def __init__(self, branch, modules, classes, doc_root, nav_order):
10
+ self.modules = modules
11
+ self.classes = classes
12
+ self.doc_root = pathlib.Path(doc_root)
13
+ self.title = branch["title"]
14
+ self.source = branch["source"]
15
+ self.nav_order = nav_order
16
+ self._attribute_cache = {}
17
+ self.args_to_additional_attributes_map = {}
18
+
19
+ def make_index_from_class_overview(self, title_snake_case, source_class, section_folder_path):
20
+ filename = "index"
21
+ section_folder_path.mkdir(exist_ok=True)
22
+
23
+ doc = self.build_header(self.title, filename, title_snake_case, None, self.nav_order, True)
24
+ (elevator_pitch, overview) = self.parse_overview_doc(
25
+ self.raw_docblock_to_md(source_class.doc).lstrip("\n").lstrip(" ")
26
+ )
27
+ doc += f"\n\n# {self.title}\n\n{elevator_pitch}\n\n## Overview\n\n{overview}"
28
+
29
+ output_file = section_folder_path / f"{filename}.md"
30
+ with output_file.open(mode="w") as doc_file:
31
+ doc_file.write(doc)
32
+
33
+ def parse_overview_doc(self, overview_doc):
34
+ parts = overview_doc.lstrip("\n").split("\n", 1)
35
+ if len(parts) < 2:
36
+ return (parts[0], "")
37
+ return (parts[0], parts[1].lstrip(" "))
38
+
39
+ def extract_attribute_docs(self, source_class, argument_names, additional_attribute_sources=[]):
40
+ """
41
+ Fetch the docblocks for class arguments.
42
+
43
+ Sadly, python doesn't support docblocks on class arguments. I only discovered this after writing all
44
+ the docblocs this way. Still, I don't want to move my docblocs, because puttig them on arguments is
45
+ legitimately the place where they make the most sense. So, we have to use the python parsing capabilities
46
+ built into python in order to extract them ourselves. Very exciting... :cry:
47
+
48
+ We substantially simplify this process (in a way that hopefully works) by setting stringent requirements
49
+ for how our docblocks need to be defined. The docblock must come before the argument and they must be
50
+ at the top of the class. In addition, when we are called, we are provided with a list of all argument
51
+ names so that we are looking for a specific list of things rather than searching more generically for
52
+ a series of documented arguments. So, we're looking for a pattern of:
53
+
54
+ 1. tokenize.STRING
55
+ 2. tokenize.NEWLINE
56
+ 3. tokenize.NAME
57
+
58
+ This will probably match a bunch of things, which is where our list of argument names comes in.
59
+ Also, we'll only use the first combination of these things we find, which menas that attribute definitions
60
+ must be at the top of the file. This will help us avoid getting confused by variable definitions with
61
+ matching names later in the class.
62
+ """
63
+ # built in classes (which we will reach with our iterative approach) don't have a source file.
64
+ if not source_class.source_file:
65
+ return {}
66
+
67
+ # we will iterate over base classes, and these often get re-used, so let's keep a cache
68
+ if source_class.source_file in self._attribute_cache:
69
+ return self._attribute_cache[source_class.source_file]
70
+
71
+ doc_strings = {}
72
+ with open(source_class.source_file, "r") as fp:
73
+ # so this is both very simple and, hopefully, not prone to failure. The tokenization information that comes back from the
74
+ # parser is surprisingly generic and vague. However, we are looking for something
75
+ last_string = ""
76
+ for token_type, token_string, (srow, scol), (erow, ecol), line_content in tokenize.generate_tokens(
77
+ fp.readline
78
+ ):
79
+ if token_type == tokenize.STRING:
80
+ last_string = token_string
81
+ continue
82
+ if token_type == tokenize.NEWLINE:
83
+ continue
84
+ if token_type != tokenize.NAME:
85
+ last_string = ""
86
+ continue
87
+ if not last_string or token_string not in argument_names:
88
+ continue
89
+ doc_strings[token_string] = last_string
90
+
91
+ # and let's repeat this for any base classes just to make sure we don't miss anything. Often attributes are defined in
92
+ # bases and we want to use those docs if we don't have them.
93
+ for base_class in source_class.base_classes:
94
+ doc_strings = {
95
+ **self.extract_attribute_docs(base_class, argument_names),
96
+ **doc_strings,
97
+ }
98
+
99
+ for additional_source_class in additional_attribute_sources:
100
+ doc_strings = {
101
+ **self.extract_attribute_docs(additional_source_class, argument_names),
102
+ **doc_strings,
103
+ }
104
+
105
+ self._attribute_cache[source_class.source_file] = doc_strings
106
+ return doc_strings
107
+
108
+ def build_header(self, title, filename, section_name, parent, nav_order, has_children):
109
+ permalink = "/docs/" + (f"{section_name}/" if section_name else "") + f"{filename}.html"
110
+ header = f"""---
111
+ layout: default
112
+ title: {title}
113
+ permalink: {permalink}
114
+ nav_order: {nav_order}
115
+ """
116
+ if parent:
117
+ header += f"parent: {parent}\n"
118
+ if has_children:
119
+ header += "has_children: true\n"
120
+ header += "---"
121
+ return header
122
+
123
+ def raw_docblock_to_md(self, docblock):
124
+ return re.sub(r"\n ", "\n", docblock)
125
+
126
+ def default_args(self):
127
+ default_args = {}
128
+ for key, value in self.args_to_additional_attributes_map.items():
129
+ parts = value.split(".")
130
+ import_path = ".".join(parts[:-1])
131
+ attribute_name = parts[-1]
132
+ source_class = self.classes.find(f"import_path={import_path}")
133
+ doc = source_class.attributes.where(f"name={attribute_name}").first().doc
134
+ if doc:
135
+ default_args[key] = self.raw_docblock_to_md(doc)
136
+ return default_args
@@ -0,0 +1,68 @@
1
+ from typing import Any
2
+ from collections import OrderedDict
3
+
4
+ import clearskies
5
+ from .builder import Builder
6
+
7
+
8
+ class Module(Builder):
9
+ def __init__(self, branch, modules, classes, doc_root, nav_order):
10
+ super().__init__(branch, modules, classes, doc_root, nav_order)
11
+ self.class_list = branch["classes"]
12
+ self.args_to_additional_attributes_map = branch.get("args_to_additional_attributes_map", {})
13
+
14
+ def build(self):
15
+ title_snake_case = clearskies.functional.string.title_case_to_snake_case(self.title.replace(" ", "")).replace(
16
+ "_", "-"
17
+ )
18
+ section_folder_path = self.doc_root / title_snake_case
19
+ source_class = self.classes.find(f"import_path={self.source}")
20
+ self.make_index_from_class_overview(title_snake_case, source_class, section_folder_path)
21
+
22
+ default_args = self.default_args()
23
+
24
+ nav_order = 0
25
+ for class_name in self.class_list:
26
+ nav_order += 1
27
+ source_class = self.classes.find(f"import_path={class_name}")
28
+ title = source_class.name
29
+ filename = clearskies.functional.string.title_case_to_snake_case(source_class.name).replace("_", "-")
30
+ class_doc = self.build_header(source_class.name, filename, title_snake_case, self.title, nav_order, False)
31
+ (elevator_pitch, overview) = self.parse_overview_doc(
32
+ self.raw_docblock_to_md(source_class.doc).lstrip("\n").lstrip(" ")
33
+ )
34
+ class_doc += f"\n\n# {title}\n\n{elevator_pitch}\n\n"
35
+ main_doc = "## Overview\n\n{overview}\n\n"
36
+ table_of_contents = " 1. [Overview](#overview)\n"
37
+
38
+ # Find the documentation for all of our init args.
39
+ arguments: dict[str, Any] = OrderedDict()
40
+ for arg in source_class.init.all_args:
41
+ if arg == "self":
42
+ continue
43
+ arguments[arg] = {
44
+ "required": arg not in source_class.init.kwargs,
45
+ "doc": default_args.get(arg, ""),
46
+ }
47
+
48
+ # for various reasons, it's easier to extract docs for all the arguments at once:
49
+ docs = self.extract_attribute_docs(source_class, list(arguments.keys()))
50
+ for arg, doc in docs.items():
51
+ # you would think that we would only get arguments that belong to our class, but this isn't the case
52
+ # because the processing caches results from parent classes, and we don't always use all attributes
53
+ # available from all our parents.
54
+ if arg not in arguments:
55
+ continue
56
+ arguments[arg]["doc"] = doc
57
+
58
+ for index, arg in enumerate(arguments.keys()):
59
+ arg_data = arguments[arg]
60
+ table_of_contents += f" {index+2}. [{arg}](#{arg})\n"
61
+ main_doc += f"## {arg}\n**" + ("Required" if arg_data["required"] else "Optional") + "**\n\n"
62
+ main_doc += self.raw_docblock_to_md(arg_data["doc"].replace('"""', "")) + "\n\n"
63
+
64
+ class_doc += f"{table_of_contents}\n{main_doc}"
65
+
66
+ output_file = section_folder_path / f"{filename}.md"
67
+ with output_file.open(mode="w") as doc_file:
68
+ doc_file.write(class_doc)
@@ -0,0 +1,78 @@
1
+ from typing import Any
2
+ from collections import OrderedDict
3
+
4
+ import clearskies
5
+ from .builder import Builder
6
+
7
+
8
+ class SingleClass(Builder):
9
+ def __init__(self, branch, modules, classes, doc_root, nav_order):
10
+ super().__init__(branch, modules, classes, doc_root, nav_order)
11
+ self.additional_attribute_sources = branch.get("additional_attribute_sources", [])
12
+ self.args_to_additional_attributes_map = branch.get("args_to_additional_attributes_map", {})
13
+ self.parent = branch.get("parent", False)
14
+
15
+ def build(self):
16
+ section_name = (
17
+ clearskies.functional.string.title_case_to_snake_case(self.parent if self.parent else self.title)
18
+ .replace("_", "-")
19
+ .replace(" ", "")
20
+ )
21
+ section_folder_path = self.doc_root / section_name
22
+ section_folder_path.mkdir(exist_ok=True)
23
+ source_class = self.classes.find(f"import_path={self.source}")
24
+
25
+ title_snake_case = clearskies.functional.string.title_case_to_snake_case(self.title.replace(" ", "")).replace(
26
+ "_", "-"
27
+ )
28
+ class_doc = self.build_header(self.title, title_snake_case, section_name, self.parent, self.nav_order, False)
29
+ (elevator_pitch, overview) = self.parse_overview_doc(
30
+ self.raw_docblock_to_md(source_class.doc).lstrip("\n").lstrip(" ")
31
+ )
32
+ class_doc += f"\n\n# {self.title}\n\n{elevator_pitch}\n\n"
33
+ main_doc = f"## Overview\n\n{overview}\n\n"
34
+ table_of_contents = " 1. [Overview](#overview)\n"
35
+
36
+ default_args = self.default_args()
37
+
38
+ # Find the documentation for all of our init args.
39
+ arguments: dict[str, Any] = OrderedDict()
40
+ for arg in source_class.init.all_args:
41
+ if arg == "self":
42
+ continue
43
+ arguments[arg] = {
44
+ "required": arg not in source_class.init.kwargs,
45
+ "doc": default_args.get(arg, ""),
46
+ }
47
+
48
+ # for various reasons, it's easier to extract docs for all the arguments at once:
49
+ docs = self.extract_attribute_docs(
50
+ source_class,
51
+ list(arguments.keys()),
52
+ additional_attribute_sources=[
53
+ self.classes.find(f"import_path={source}") for source in self.additional_attribute_sources
54
+ ],
55
+ )
56
+ for arg, doc in docs.items():
57
+ # you would think that we would only get arguments that belong to our class, but this isn't the case
58
+ # because the processing caches results from parent classes, and we don't always use all attributes
59
+ # available from all our parents.
60
+ if arg not in arguments:
61
+ continue
62
+ arguments[arg]["doc"] = doc
63
+
64
+ for index, arg in enumerate(arguments.keys()):
65
+ arg_data = arguments[arg]
66
+ table_of_contents += f" {index+2}. [{arg}](#{arg})\n"
67
+ main_doc += f"## {arg}\n**" + ("Required" if arg_data["required"] else "Optional") + "**\n\n"
68
+ main_doc += self.raw_docblock_to_md(arg_data["doc"].replace('"""', "")) + "\n\n"
69
+
70
+ class_doc += f"{table_of_contents}\n{main_doc}"
71
+
72
+ output_file = section_folder_path / (
73
+ "index.md"
74
+ if not self.parent
75
+ else clearskies.functional.string.title_case_to_snake_case(self.title.replace(" ", "")) + ".md"
76
+ )
77
+ with output_file.open(mode="w") as doc_file:
78
+ doc_file.write(class_doc)
@@ -0,0 +1,38 @@
1
+ import re
2
+
3
+ import clearskies
4
+ from .builder import Builder
5
+
6
+
7
+ class SingleClassToSection(Builder):
8
+ def __init__(self, branch, modules, classes, doc_root, nav_order):
9
+ super().__init__(branch, modules, classes, doc_root, nav_order)
10
+ self.docs = branch["docs"]
11
+
12
+ def build(self):
13
+ section_name = clearskies.functional.string.title_case_to_snake_case(self.title).replace("_", "-")
14
+ section_folder_path = self.doc_root / section_name
15
+ source_class = self.classes.find(f"import_path={self.source}")
16
+ self.make_index_from_class_overview(section_name, source_class, section_folder_path)
17
+
18
+ for index, doc_data in enumerate(self.docs):
19
+ title = doc_data["title"]
20
+ title_snake_case = clearskies.functional.string.title_case_to_snake_case(title.replace(" ", "")).replace(
21
+ "_", "-"
22
+ )
23
+ doc = self.build_header(title, title_snake_case, section_name, self.title, index + 1, False)
24
+ doc += f"\n\n# {title}\n\n"
25
+ table_of_contents = ""
26
+ attribute_docs = ""
27
+
28
+ for index, attribute_name in enumerate(doc_data["attributes"]):
29
+ attribute = source_class.attributes.find(f"name={attribute_name}")
30
+ table_of_contents += f" {index+1}. [{attribute_name}]({title_snake_case}.html#{attribute_name})\n"
31
+ attribute_docs += f"\n\n## {attribute_name}\n\n"
32
+ attribute_docs += re.sub("\n ", "\n", self.raw_docblock_to_md(attribute.doc))
33
+
34
+ doc += f"{table_of_contents}{attribute_docs}"
35
+
36
+ output_file = section_folder_path / f"{title_snake_case}.md"
37
+ with output_file.open(mode="w") as doc_file:
38
+ doc_file.write(doc)
@@ -0,0 +1,17 @@
1
+ from clearskies_doc_builder.columns.any import Any
2
+ from clearskies_doc_builder.columns.attribute import Attribute
3
+ from clearskies_doc_builder.columns.attributes import Attributes
4
+ from clearskies_doc_builder.columns.base_classes import BaseClasses
5
+ from clearskies_doc_builder.columns.class_column import Class
6
+ from clearskies_doc_builder.columns.module import Module
7
+ from clearskies_doc_builder.columns.module_classes import ModuleClasses
8
+
9
+ __all__ = [
10
+ "Any",
11
+ "Attribute",
12
+ "Attributes",
13
+ "BaseClasses",
14
+ "Class",
15
+ "Module",
16
+ "ModuleClasses",
17
+ ]
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+ from typing import overload, Self, TYPE_CHECKING
3
+ from typing import Any as AnyType
4
+
5
+ import clearskies
6
+
7
+ if TYPE_CHECKING:
8
+ from clearskies import Model
9
+
10
+
11
+ class Any(clearskies.Column):
12
+ is_writeable = clearskies.configs.boolean.Boolean(default=False)
13
+ _descriptor_config_map = None
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+
18
+ @overload
19
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
20
+ pass
21
+
22
+ @overload
23
+ def __get__(self, instance: Model, cls: type[Model]) -> AnyType:
24
+ pass
25
+
26
+ def __get__(self, instance, cls):
27
+ return super().__get__(instance, cls)
28
+
29
+ def __set__(self, instance: Model, value: AnyType) -> None:
30
+ instance._next_data[self.name] = value
31
+
32
+ def from_backend(self, value):
33
+ return value
34
+
35
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
36
+ if self.name not in data:
37
+ return data
38
+ return {**data, self.name: data[self.name]}
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+ from typing import Callable
3
+
4
+ import clearskies
5
+
6
+
7
+ class Attribute(clearskies.columns.HasMany):
8
+ def __init__(
9
+ self,
10
+ child_model_class,
11
+ readable_child_column_names: list[str] = [],
12
+ filter: Callable | None = None,
13
+ ):
14
+ self.filter = filter
15
+ super().__init__(
16
+ child_model_class,
17
+ foreign_column_name="parent_class",
18
+ readable_child_column_names=readable_child_column_names,
19
+ )
20
+
21
+ def __get__(self, model, cls):
22
+ if model is None:
23
+ self.model_class = cls
24
+ return self # type: ignore
25
+
26
+ # this makes sure we're initialized
27
+ if "name" not in self._config:
28
+ model.get_columns()
29
+
30
+ attributes = self.child_model.where(self.child_model_class.parent_class.equals(model.type))
31
+ if self.filter:
32
+ return list(filter(self.filter, attributes))[0]
33
+
34
+ return attributes[0]
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+ from typing import Callable
3
+
4
+ import clearskies
5
+
6
+
7
+ class Attributes(clearskies.columns.HasMany):
8
+ def __init__(
9
+ self,
10
+ child_model_class,
11
+ readable_child_column_names: list[str] = [],
12
+ filter: Callable | None = None,
13
+ ):
14
+ self.filter = filter
15
+ super().__init__(
16
+ child_model_class,
17
+ foreign_column_name="parent_class",
18
+ readable_child_column_names=readable_child_column_names,
19
+ )
20
+
21
+ def __get__(self, model, cls):
22
+ if model is None:
23
+ self.model_class = cls
24
+ return self # type: ignore
25
+
26
+ # this makes sure we're initialized
27
+ if "name" not in self._config:
28
+ model.get_columns()
29
+
30
+ attributes = self.child_model.where(self.child_model_class.parent_class.equals(model.type))
31
+ if self.filter:
32
+ return list(filter(self.filter, attributes))
33
+
34
+ return attributes
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import clearskies
4
+
5
+
6
+ class BaseClasses(clearskies.columns.HasMany):
7
+ def __init__(
8
+ self,
9
+ readable_child_column_names: list[str] = [],
10
+ ):
11
+ self.foreign_column_name = "type"
12
+ self.readable_child_column_names = readable_child_column_names
13
+
14
+ def finalize_configuration(self, model_class, name) -> None:
15
+ self.child_model_class = model_class
16
+ super().finalize_configuration(model_class, name)
17
+
18
+ def __get__(self, model, cls):
19
+ if model is None:
20
+ self.model_class = cls
21
+ return self # type: ignore
22
+
23
+ # this makes sure we're initialized
24
+ if "name" not in self._config:
25
+ model.get_columns()
26
+
27
+ bases = []
28
+ for cls in model.type.__bases__:
29
+ bases.append(model.model(model.backend.unpack(cls, model.module)))
30
+
31
+ return bases
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+ from typing import Any, overload, Self, TYPE_CHECKING
3
+
4
+ import clearskies
5
+
6
+ if TYPE_CHECKING:
7
+ from clearskies import Model
8
+
9
+
10
+ class Class(clearskies.Column):
11
+ is_writeable = clearskies.configs.boolean.Boolean(default=False)
12
+ _descriptor_config_map = None
13
+
14
+ def __init__(self):
15
+ super().__init__()
16
+
17
+ @overload
18
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
19
+ pass
20
+
21
+ @overload
22
+ def __get__(self, instance: Model, cls: type[Model]) -> type:
23
+ pass
24
+
25
+ def __get__(self, instance, cls):
26
+ return super().__get__(instance, cls)
27
+
28
+ def __set__(self, instance: Model, value: type) -> None:
29
+ instance._next_data[self.name] = value
30
+
31
+ def from_backend(self, value):
32
+ return value
33
+
34
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
35
+ if self.name not in data:
36
+ return data
37
+ return {**data, self.name: data[self.name]}
File without changes
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+ from typing import Any, overload, Self, TYPE_CHECKING
3
+ from types import ModuleType
4
+
5
+ import clearskies
6
+
7
+ if TYPE_CHECKING:
8
+ from clearskies import Model
9
+
10
+
11
+ class Module(clearskies.Column):
12
+ is_writeable = clearskies.configs.boolean.Boolean(default=False)
13
+ _descriptor_config_map = None
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+
18
+ @overload
19
+ def __get__(self, instance: None, cls: type[Model]) -> Self:
20
+ pass
21
+
22
+ @overload
23
+ def __get__(self, instance: Model, cls: type[Model]) -> ModuleType:
24
+ pass
25
+
26
+ def __get__(self, instance, cls):
27
+ return super().__get__(instance, cls)
28
+
29
+ def __set__(self, instance: Model, value: ModuleType) -> None:
30
+ instance._next_data[self.name] = value
31
+
32
+ def from_backend(self, value):
33
+ return value
34
+
35
+ def to_backend(self, data: dict[str, Any]) -> dict[str, Any]:
36
+ if self.name not in data:
37
+ return data
38
+ return {**data, self.name: data[self.name]}
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import clearskies
4
+
5
+
6
+ class ModuleClasses(clearskies.columns.HasMany):
7
+ def __init__(
8
+ self,
9
+ child_model_class,
10
+ readable_child_column_names: list[str] = [],
11
+ ):
12
+ super().__init__(
13
+ child_model_class,
14
+ foreign_column_name="module",
15
+ readable_child_column_names=readable_child_column_names,
16
+ )
17
+
18
+ def __get__(self, model, cls):
19
+ if model is None:
20
+ self.model_class = cls
21
+ return self # type: ignore
22
+
23
+ # this makes sure we're initialized
24
+ if "name" not in self._config:
25
+ model.get_columns()
26
+
27
+ return self.child_model.where(cls.module.equals(model.module))
@@ -0,0 +1,13 @@
1
+ from clearskies_doc_builder.models.attribute import Attribute
2
+ from clearskies_doc_builder.models.method import Method
3
+ from clearskies_doc_builder.models.module import Module
4
+ from clearskies_doc_builder.models.class_model import Class
5
+ from clearskies_doc_builder.models.class_reference import ClassReference
6
+
7
+ __all__ = [
8
+ "Attribute",
9
+ "Class",
10
+ "ClassReference",
11
+ "Method",
12
+ "Module",
13
+ ]
File without changes
@@ -0,0 +1,14 @@
1
+ import clearskies
2
+ from clearskies_doc_builder import backends, columns
3
+
4
+
5
+ class Attribute(clearskies.Model):
6
+ id_column_name = "id"
7
+ backend = backends.AttributeBackend()
8
+
9
+ id = clearskies.columns.Integer()
10
+ name = clearskies.columns.String()
11
+ type = columns.Class()
12
+ doc = clearskies.columns.String()
13
+ attribute = columns.Any()
14
+ parent_class = columns.Class()
@@ -0,0 +1,7 @@
1
+ from clearskies.model import ModelClassReference
2
+ from . import attribute
3
+
4
+
5
+ class AttributeReference(ModelClassReference):
6
+ def get_model_class(self):
7
+ return attribute.Attribute
@@ -0,0 +1,24 @@
1
+ import clearskies
2
+ from clearskies_doc_builder import backends, columns
3
+ from .attribute_reference import AttributeReference
4
+ from .method_reference import MethodReference
5
+
6
+
7
+ class Class(clearskies.Model):
8
+ id_column_name = "id"
9
+ backend = backends.ClassBackend()
10
+
11
+ id = clearskies.columns.Integer()
12
+ type = columns.Any()
13
+ source_file = clearskies.columns.String()
14
+ import_path = clearskies.columns.String()
15
+ name = clearskies.columns.String(is_searchable=False)
16
+ doc = clearskies.columns.String(is_searchable=False)
17
+ module = columns.Module()
18
+ base_classes = columns.BaseClasses()
19
+ attributes = columns.Attributes(AttributeReference)
20
+ methods = columns.Attributes(AttributeReference, filter=lambda attribute: callable(attribute.attribute))
21
+ init = columns.Attribute(
22
+ MethodReference,
23
+ filter=lambda attribute: attribute.name == "__init__",
24
+ )
@@ -0,0 +1,7 @@
1
+ from clearskies.model import ModelClassReference
2
+ from . import class_model
3
+
4
+
5
+ class ClassReference(ModelClassReference):
6
+ def get_model_class(self):
7
+ return class_model.Class
@@ -0,0 +1,19 @@
1
+ import clearskies
2
+ from clearskies_doc_builder import backends, columns
3
+
4
+
5
+ class Method(clearskies.Model):
6
+ id_column_name = "id"
7
+ backend = backends.AttributeBackend()
8
+
9
+ id = clearskies.columns.Integer()
10
+ name = clearskies.columns.String()
11
+ type = columns.Class()
12
+ doc = clearskies.columns.String()
13
+ attribute = columns.Any()
14
+ parent_class = columns.Class()
15
+
16
+ args = columns.Any()
17
+ kwargs = columns.Any()
18
+ all_args = columns.Any()
19
+ defaults = columns.Any()
@@ -0,0 +1,7 @@
1
+ from clearskies.model import ModelClassReference
2
+ from . import method
3
+
4
+
5
+ class MethodReference(ModelClassReference):
6
+ def get_model_class(self):
7
+ return method.Method
@@ -0,0 +1,17 @@
1
+ import clearskies
2
+ from clearskies_doc_builder import backends, columns
3
+ from . import class_reference
4
+
5
+
6
+ class Module(clearskies.Model):
7
+ id_column_name = "id"
8
+ backend = backends.ModuleBackend()
9
+
10
+ id = clearskies.columns.Integer()
11
+ import_path = clearskies.columns.String()
12
+ source_file = clearskies.columns.String()
13
+ is_builtin = clearskies.columns.Boolean()
14
+ name = clearskies.columns.String()
15
+ doc = clearskies.columns.String(is_searchable=False)
16
+ module = columns.Module()
17
+ classes = columns.ModuleClasses(class_reference.ClassReference)
@@ -0,0 +1,7 @@
1
+ from clearskies.model import ModelClassReference
2
+ from . import module
3
+
4
+
5
+ class ModuleReference(ModelClassReference):
6
+ def get_model_class(self):
7
+ return module.Module
File without changes
@@ -0,0 +1,22 @@
1
+ import shutil
2
+ import pathlib
3
+
4
+
5
+ def prepare_doc_space(project_root):
6
+ project_path = pathlib.Path(project_root)
7
+ build_path = project_path / "build"
8
+ doc_path = build_path / "docs"
9
+ jekyll_path = project_path / "jekyll"
10
+
11
+ if doc_path.is_dir():
12
+ shutil.rmtree(doc_path)
13
+ build_path.mkdir(parents=True, exist_ok=True)
14
+
15
+ for file in jekyll_path.glob("*"):
16
+ if not file.is_file():
17
+ continue
18
+ shutil.copy2(str(file), str(build_path / file.name))
19
+
20
+ shutil.copytree(str(jekyll_path / "docs"), str(build_path / "docs"))
21
+
22
+ return str(doc_path)