pyrig 2.2.6__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.
- pyrig/__init__.py +1 -0
- pyrig/dev/__init__.py +6 -0
- pyrig/dev/builders/__init__.py +1 -0
- pyrig/dev/builders/base/__init__.py +5 -0
- pyrig/dev/builders/base/base.py +256 -0
- pyrig/dev/builders/pyinstaller.py +229 -0
- pyrig/dev/cli/__init__.py +5 -0
- pyrig/dev/cli/cli.py +95 -0
- pyrig/dev/cli/commands/__init__.py +1 -0
- pyrig/dev/cli/commands/build_artifacts.py +16 -0
- pyrig/dev/cli/commands/create_root.py +25 -0
- pyrig/dev/cli/commands/create_tests.py +244 -0
- pyrig/dev/cli/commands/init_project.py +160 -0
- pyrig/dev/cli/commands/make_inits.py +27 -0
- pyrig/dev/cli/commands/protect_repo.py +145 -0
- pyrig/dev/cli/shared_subcommands.py +20 -0
- pyrig/dev/cli/subcommands.py +73 -0
- pyrig/dev/configs/__init__.py +1 -0
- pyrig/dev/configs/base/__init__.py +5 -0
- pyrig/dev/configs/base/base.py +826 -0
- pyrig/dev/configs/containers/__init__.py +1 -0
- pyrig/dev/configs/containers/container_file.py +111 -0
- pyrig/dev/configs/dot_env.py +95 -0
- pyrig/dev/configs/dot_python_version.py +88 -0
- pyrig/dev/configs/git/__init__.py +5 -0
- pyrig/dev/configs/git/gitignore.py +181 -0
- pyrig/dev/configs/git/pre_commit.py +170 -0
- pyrig/dev/configs/licence.py +112 -0
- pyrig/dev/configs/markdown/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/index.py +38 -0
- pyrig/dev/configs/markdown/readme.py +132 -0
- pyrig/dev/configs/py_typed.py +28 -0
- pyrig/dev/configs/pyproject.py +436 -0
- pyrig/dev/configs/python/__init__.py +5 -0
- pyrig/dev/configs/python/builders_init.py +27 -0
- pyrig/dev/configs/python/configs_init.py +28 -0
- pyrig/dev/configs/python/dot_experiment.py +46 -0
- pyrig/dev/configs/python/main.py +59 -0
- pyrig/dev/configs/python/resources_init.py +27 -0
- pyrig/dev/configs/python/shared_subcommands.py +29 -0
- pyrig/dev/configs/python/src_init.py +27 -0
- pyrig/dev/configs/python/subcommands.py +27 -0
- pyrig/dev/configs/testing/__init__.py +5 -0
- pyrig/dev/configs/testing/conftest.py +64 -0
- pyrig/dev/configs/testing/fixtures_init.py +27 -0
- pyrig/dev/configs/testing/main_test.py +74 -0
- pyrig/dev/configs/testing/zero_test.py +43 -0
- pyrig/dev/configs/workflows/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/base.py +1662 -0
- pyrig/dev/configs/workflows/build.py +106 -0
- pyrig/dev/configs/workflows/health_check.py +133 -0
- pyrig/dev/configs/workflows/publish.py +68 -0
- pyrig/dev/configs/workflows/release.py +90 -0
- pyrig/dev/tests/__init__.py +5 -0
- pyrig/dev/tests/conftest.py +40 -0
- pyrig/dev/tests/fixtures/__init__.py +1 -0
- pyrig/dev/tests/fixtures/assertions.py +147 -0
- pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
- pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
- pyrig/dev/tests/fixtures/autouse/module.py +40 -0
- pyrig/dev/tests/fixtures/autouse/session.py +589 -0
- pyrig/dev/tests/fixtures/factories.py +118 -0
- pyrig/dev/utils/__init__.py +1 -0
- pyrig/dev/utils/cli.py +17 -0
- pyrig/dev/utils/git.py +312 -0
- pyrig/dev/utils/packages.py +93 -0
- pyrig/dev/utils/resources.py +77 -0
- pyrig/dev/utils/testing.py +66 -0
- pyrig/dev/utils/versions.py +268 -0
- pyrig/main.py +9 -0
- pyrig/py.typed +0 -0
- pyrig/resources/GITIGNORE +216 -0
- pyrig/resources/LATEST_PYTHON_VERSION +1 -0
- pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
- pyrig/resources/__init__.py +1 -0
- pyrig/src/__init__.py +1 -0
- pyrig/src/git/__init__.py +6 -0
- pyrig/src/git/git.py +146 -0
- pyrig/src/graph.py +255 -0
- pyrig/src/iterate.py +107 -0
- pyrig/src/modules/__init__.py +22 -0
- pyrig/src/modules/class_.py +369 -0
- pyrig/src/modules/function.py +189 -0
- pyrig/src/modules/inspection.py +148 -0
- pyrig/src/modules/module.py +658 -0
- pyrig/src/modules/package.py +452 -0
- pyrig/src/os/__init__.py +6 -0
- pyrig/src/os/os.py +121 -0
- pyrig/src/project/__init__.py +5 -0
- pyrig/src/project/mgt.py +83 -0
- pyrig/src/resource.py +58 -0
- pyrig/src/string.py +100 -0
- pyrig/src/testing/__init__.py +6 -0
- pyrig/src/testing/assertions.py +66 -0
- pyrig/src/testing/convention.py +203 -0
- pyrig-2.2.6.dist-info/METADATA +174 -0
- pyrig-2.2.6.dist-info/RECORD +102 -0
- pyrig-2.2.6.dist-info/WHEEL +4 -0
- pyrig-2.2.6.dist-info/entry_points.txt +3 -0
- pyrig-2.2.6.dist-info/licenses/LICENSE +21 -0
pyrig/src/graph.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Directed graph implementation for dependency analysis.
|
|
2
|
+
|
|
3
|
+
This module provides a simple but efficient directed graph (DiGraph) data structure
|
|
4
|
+
used primarily for analyzing Python package dependency relationships. The graph
|
|
5
|
+
supports bidirectional traversal, enabling both "what does X depend on" and
|
|
6
|
+
"what depends on X" queries.
|
|
7
|
+
|
|
8
|
+
The implementation maintains both forward and reverse edge mappings for O(1)
|
|
9
|
+
neighbor lookups in either direction, at the cost of doubled memory for edges.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> graph = DiGraph()
|
|
13
|
+
>>> graph.add_edge("app", "library")
|
|
14
|
+
>>> graph.add_edge("library", "utils")
|
|
15
|
+
>>> graph.ancestors("utils") # What depends on utils?
|
|
16
|
+
{'app', 'library'}
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DiGraph:
|
|
21
|
+
"""A directed graph with efficient bidirectional traversal.
|
|
22
|
+
|
|
23
|
+
This graph implementation maintains both forward edges (node -> dependencies)
|
|
24
|
+
and reverse edges (node -> dependents) to support efficient queries in both
|
|
25
|
+
directions. It is designed for package dependency analysis where you often
|
|
26
|
+
need to find all packages that depend on a given package.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
_nodes: Set of all node identifiers in the graph.
|
|
30
|
+
_edges: Forward adjacency mapping (node -> set of outgoing neighbors).
|
|
31
|
+
_reverse_edges: Reverse adjacency mapping (node -> set of incoming neighbors).
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> g = DiGraph()
|
|
35
|
+
>>> g.add_edge("A", "B") # A depends on B
|
|
36
|
+
>>> g.add_edge("A", "C") # A depends on C
|
|
37
|
+
>>> g["A"] # What does A depend on?
|
|
38
|
+
{'B', 'C'}
|
|
39
|
+
>>> g.ancestors("B") # What depends on B?
|
|
40
|
+
{'A'}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
"""Initialize an empty directed graph with no nodes or edges."""
|
|
45
|
+
self._nodes: set[str] = set()
|
|
46
|
+
self._edges: dict[str, set[str]] = {} # node -> outgoing neighbors
|
|
47
|
+
self._reverse_edges: dict[str, set[str]] = {} # node -> incoming neighbors
|
|
48
|
+
|
|
49
|
+
def add_node(self, node: str) -> None:
|
|
50
|
+
"""Add a node to the graph if it doesn't already exist.
|
|
51
|
+
|
|
52
|
+
Initializes empty edge sets for the node in both forward and reverse
|
|
53
|
+
adjacency mappings.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
node: The identifier for the node to add.
|
|
57
|
+
"""
|
|
58
|
+
self._nodes.add(node)
|
|
59
|
+
if node not in self._edges:
|
|
60
|
+
self._edges[node] = set()
|
|
61
|
+
if node not in self._reverse_edges:
|
|
62
|
+
self._reverse_edges[node] = set()
|
|
63
|
+
|
|
64
|
+
def add_edge(self, source: str, target: str) -> None:
|
|
65
|
+
"""Add a directed edge from source to target.
|
|
66
|
+
|
|
67
|
+
Both nodes are automatically added to the graph if they don't exist.
|
|
68
|
+
The edge represents that `source` depends on `target`.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
source: The node that depends on target (edge origin).
|
|
72
|
+
target: The node that source depends on (edge destination).
|
|
73
|
+
"""
|
|
74
|
+
self.add_node(source)
|
|
75
|
+
self.add_node(target)
|
|
76
|
+
self._edges[source].add(target)
|
|
77
|
+
self._reverse_edges[target].add(source)
|
|
78
|
+
|
|
79
|
+
def __contains__(self, node: str) -> bool:
|
|
80
|
+
"""Check if a node exists in the graph.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
node: The node identifier to check.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if the node is in the graph, False otherwise.
|
|
87
|
+
"""
|
|
88
|
+
return node in self._nodes
|
|
89
|
+
|
|
90
|
+
def __getitem__(self, node: str) -> set[str]:
|
|
91
|
+
"""Get the outgoing neighbors (dependencies) of a node.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
node: The node to get dependencies for.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Set of nodes that the given node depends on. Returns empty set
|
|
98
|
+
if the node doesn't exist or has no dependencies.
|
|
99
|
+
"""
|
|
100
|
+
return self._edges.get(node, set())
|
|
101
|
+
|
|
102
|
+
def nodes(self) -> set[str]:
|
|
103
|
+
"""Return all nodes in the graph.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A set containing all node identifiers in the graph.
|
|
107
|
+
"""
|
|
108
|
+
return self._nodes
|
|
109
|
+
|
|
110
|
+
def has_edge(self, source: str, target: str) -> bool:
|
|
111
|
+
"""Check if a directed edge exists from source to target.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
source: The potential edge origin.
|
|
115
|
+
target: The potential edge destination.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if an edge exists from source to target, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
return target in self._edges.get(source, set())
|
|
121
|
+
|
|
122
|
+
def ancestors(self, target: str) -> set[str]:
|
|
123
|
+
"""Find all nodes that can reach the target node (transitive dependents).
|
|
124
|
+
|
|
125
|
+
Performs a breadth-first traversal of the reverse edges to find all
|
|
126
|
+
nodes that directly or indirectly depend on the target. This is useful
|
|
127
|
+
for determining the "blast radius" of a change to a package.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
target: The node to find ancestors for.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Set of all nodes that have a path to target (i.e., all packages
|
|
134
|
+
that depend on target, directly or transitively). Does not include
|
|
135
|
+
target itself.
|
|
136
|
+
"""
|
|
137
|
+
if target not in self:
|
|
138
|
+
return set()
|
|
139
|
+
|
|
140
|
+
visited: set[str] = set()
|
|
141
|
+
queue = list(self._reverse_edges.get(target, set()))
|
|
142
|
+
|
|
143
|
+
while queue:
|
|
144
|
+
node = queue.pop(0)
|
|
145
|
+
if node not in visited:
|
|
146
|
+
visited.add(node)
|
|
147
|
+
queue.extend(self._reverse_edges.get(node, set()) - visited)
|
|
148
|
+
|
|
149
|
+
return visited
|
|
150
|
+
|
|
151
|
+
def shortest_path_length(self, source: str, target: str) -> int:
|
|
152
|
+
"""Find the shortest path length between source and target.
|
|
153
|
+
|
|
154
|
+
Uses breadth-first search to find the minimum number of edges
|
|
155
|
+
between two nodes. Useful for determining dependency depth.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
source: The starting node.
|
|
159
|
+
target: The destination node.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The number of edges in the shortest path from source to target.
|
|
163
|
+
Returns 0 if source and target are the same node.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If either node is not in the graph, or if no path
|
|
167
|
+
exists between the nodes.
|
|
168
|
+
"""
|
|
169
|
+
if source not in self or target not in self:
|
|
170
|
+
msg = f"Node not in graph: {source if source not in self else target}"
|
|
171
|
+
raise ValueError(msg)
|
|
172
|
+
|
|
173
|
+
if source == target:
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
visited: set[str] = {source}
|
|
177
|
+
queue: list[tuple[str, int]] = [(source, 0)]
|
|
178
|
+
|
|
179
|
+
while queue:
|
|
180
|
+
node, distance = queue.pop(0)
|
|
181
|
+
for neighbor in self._edges.get(node, set()):
|
|
182
|
+
if neighbor == target:
|
|
183
|
+
return distance + 1
|
|
184
|
+
if neighbor not in visited:
|
|
185
|
+
visited.add(neighbor)
|
|
186
|
+
queue.append((neighbor, distance + 1))
|
|
187
|
+
|
|
188
|
+
msg = f"No path from {source} to {target}"
|
|
189
|
+
raise ValueError(msg)
|
|
190
|
+
|
|
191
|
+
def topological_sort_subgraph(self, nodes: set[str]) -> list[str]:
|
|
192
|
+
"""Topologically sort a subset of nodes in the graph.
|
|
193
|
+
|
|
194
|
+
Performs Kahn's algorithm for topological sorting on the specified
|
|
195
|
+
subset of nodes. The result is ordered such that dependencies come
|
|
196
|
+
before their dependents.
|
|
197
|
+
|
|
198
|
+
In the dependency graph, an edge A → B means "A depends on B".
|
|
199
|
+
The topological sort ensures B appears before A in the result.
|
|
200
|
+
|
|
201
|
+
This is useful for ordering packages by their dependency relationships,
|
|
202
|
+
ensuring that dependencies are processed before their dependents.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
nodes: Set of node identifiers to sort. Only edges between nodes
|
|
206
|
+
in this set are considered.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of nodes in topological order (dependencies before dependents).
|
|
210
|
+
If multiple valid orderings exist, the result is deterministic but
|
|
211
|
+
arbitrary among the valid orderings.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If the subgraph contains a cycle, making topological
|
|
215
|
+
sort impossible.
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
>>> g = DiGraph()
|
|
219
|
+
>>> g.add_edge("pkg2", "pkg1") # pkg2 depends on pkg1
|
|
220
|
+
>>> g.add_edge("pkg1", "pyrig") # pkg1 depends on pyrig
|
|
221
|
+
>>> g.topological_sort_subgraph({"pyrig", "pkg1", "pkg2"})
|
|
222
|
+
['pyrig', 'pkg1', 'pkg2'] # Dependencies first
|
|
223
|
+
"""
|
|
224
|
+
# Count outgoing edges (dependencies) for each node in the subgraph
|
|
225
|
+
# Nodes with 0 outgoing edges have no dependencies
|
|
226
|
+
out_degree: dict[str, int] = dict.fromkeys(nodes, 0)
|
|
227
|
+
|
|
228
|
+
for node in nodes:
|
|
229
|
+
for dependency in self._edges.get(node, set()):
|
|
230
|
+
if dependency in nodes:
|
|
231
|
+
out_degree[node] += 1
|
|
232
|
+
|
|
233
|
+
# Start with nodes that have no dependencies in the subgraph
|
|
234
|
+
queue = [node for node in nodes if out_degree[node] == 0]
|
|
235
|
+
result: list[str] = []
|
|
236
|
+
|
|
237
|
+
while queue:
|
|
238
|
+
# Sort queue for deterministic ordering
|
|
239
|
+
queue.sort()
|
|
240
|
+
node = queue.pop(0)
|
|
241
|
+
result.append(node)
|
|
242
|
+
|
|
243
|
+
# For each package that depends on this node (reverse edges)
|
|
244
|
+
for dependent in self._reverse_edges.get(node, set()):
|
|
245
|
+
if dependent in nodes:
|
|
246
|
+
out_degree[dependent] -= 1
|
|
247
|
+
if out_degree[dependent] == 0:
|
|
248
|
+
queue.append(dependent)
|
|
249
|
+
|
|
250
|
+
# Check for cycles
|
|
251
|
+
if len(result) != len(nodes):
|
|
252
|
+
msg = "Cycle detected in subgraph, cannot topologically sort"
|
|
253
|
+
raise ValueError(msg)
|
|
254
|
+
|
|
255
|
+
return result
|
pyrig/src/iterate.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Utilities for iterating over data structures."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def nested_structure_is_subset( # noqa: C901
|
|
11
|
+
subset: dict[Any, Any] | list[Any] | Any,
|
|
12
|
+
superset: dict[Any, Any] | list[Any] | Any,
|
|
13
|
+
on_false_dict_action: Callable[[dict[Any, Any], dict[Any, Any], Any], Any]
|
|
14
|
+
| None = None,
|
|
15
|
+
on_false_list_action: Callable[[list[Any], list[Any], int], Any] | None = None,
|
|
16
|
+
) -> bool:
|
|
17
|
+
"""Check if a nested structure is a subset of another nested structure.
|
|
18
|
+
|
|
19
|
+
Performs deep comparison of nested dictionaries and lists to verify that
|
|
20
|
+
all keys/values in `subset` exist in `superset`. This enables validation
|
|
21
|
+
that required configuration values are present while allowing additional
|
|
22
|
+
values in the superset.
|
|
23
|
+
|
|
24
|
+
The comparison rules are:
|
|
25
|
+
- Dictionaries: All keys in subset must exist in superset with matching
|
|
26
|
+
values (superset may have additional keys).
|
|
27
|
+
- Lists: All items in subset must exist somewhere in superset
|
|
28
|
+
(order does not matter, superset may have additional items).
|
|
29
|
+
- Primitives: Must be exactly equal.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
subset: The structure that should be contained within superset.
|
|
33
|
+
Can be a dict, list, or primitive value.
|
|
34
|
+
superset: The structure to check against. Should contain all
|
|
35
|
+
elements from subset (and possibly more).
|
|
36
|
+
on_false_dict_action: Optional callback invoked when a dict comparison
|
|
37
|
+
fails. Receives (subset_dict, superset_dict, failing_key). Can
|
|
38
|
+
modify the structures to fix the mismatch; comparison is retried
|
|
39
|
+
after the action.
|
|
40
|
+
on_false_list_action: Optional callback invoked when a list comparison
|
|
41
|
+
fails. Receives (subset_list, superset_list, failing_index). Can
|
|
42
|
+
modify the structures to fix the mismatch; comparison is retried
|
|
43
|
+
after the action.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if all elements in subset exist in superset with matching values,
|
|
47
|
+
False otherwise.
|
|
48
|
+
|
|
49
|
+
Note:
|
|
50
|
+
The optional action callbacks enable auto-correction behavior: when a
|
|
51
|
+
mismatch is found, the callback can modify the superset to include the
|
|
52
|
+
missing value, and the comparison is retried. This is used by ConfigFile
|
|
53
|
+
to automatically add missing required settings to config files.
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(subset, dict) and isinstance(superset, dict):
|
|
56
|
+
iterable: Iterable[tuple[Any, Any]] = subset.items()
|
|
57
|
+
on_false_action: Callable[[Any, Any, Any], Any] | None = on_false_dict_action
|
|
58
|
+
|
|
59
|
+
def get_actual(key_or_index: Any) -> Any:
|
|
60
|
+
"""Get actual value from superset."""
|
|
61
|
+
return superset.get(key_or_index)
|
|
62
|
+
|
|
63
|
+
elif isinstance(subset, list) and isinstance(superset, list):
|
|
64
|
+
iterable = enumerate(subset)
|
|
65
|
+
on_false_action = on_false_list_action
|
|
66
|
+
|
|
67
|
+
def get_actual(key_or_index: Any) -> Any:
|
|
68
|
+
"""Get actual value from superset."""
|
|
69
|
+
subset_val = subset[key_or_index]
|
|
70
|
+
for superset_val in superset:
|
|
71
|
+
if nested_structure_is_subset(subset_val, superset_val):
|
|
72
|
+
return superset_val
|
|
73
|
+
|
|
74
|
+
return superset[key_or_index] if key_or_index < len(superset) else None
|
|
75
|
+
else:
|
|
76
|
+
return subset == superset
|
|
77
|
+
|
|
78
|
+
all_good = True
|
|
79
|
+
for key_or_index, value in iterable:
|
|
80
|
+
actual_value = get_actual(key_or_index)
|
|
81
|
+
if not nested_structure_is_subset(
|
|
82
|
+
value, actual_value, on_false_dict_action, on_false_list_action
|
|
83
|
+
):
|
|
84
|
+
all_good = False
|
|
85
|
+
if on_false_action is not None:
|
|
86
|
+
on_false_action(subset, superset, key_or_index) # ty:ignore[invalid-argument-type]
|
|
87
|
+
all_good = nested_structure_is_subset(subset, superset)
|
|
88
|
+
|
|
89
|
+
if not all_good:
|
|
90
|
+
# make an informational log
|
|
91
|
+
logger.debug(
|
|
92
|
+
"""
|
|
93
|
+
-------------------------------------------------------------------------------
|
|
94
|
+
Subset:
|
|
95
|
+
%s
|
|
96
|
+
-------------------
|
|
97
|
+
is not a subset of
|
|
98
|
+
-------------------
|
|
99
|
+
Superset:
|
|
100
|
+
%s
|
|
101
|
+
-------------------------------------------------------------------------------
|
|
102
|
+
""",
|
|
103
|
+
subset,
|
|
104
|
+
superset,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return all_good
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Python module and package introspection utilities.
|
|
2
|
+
|
|
3
|
+
This package provides comprehensive utilities for working with Python's module
|
|
4
|
+
system, including module discovery, class introspection, function extraction,
|
|
5
|
+
and package traversal. These tools power pyrig's automatic discovery of
|
|
6
|
+
ConfigFile subclasses, Builder implementations, and test fixtures.
|
|
7
|
+
|
|
8
|
+
Modules:
|
|
9
|
+
class_: Class introspection utilities including method extraction and
|
|
10
|
+
subclass discovery with intelligent parent class filtering.
|
|
11
|
+
function: Function detection and extraction utilities for identifying
|
|
12
|
+
callable objects in modules.
|
|
13
|
+
inspection: Low-level inspection utilities for unwrapping decorators
|
|
14
|
+
and accessing object metadata.
|
|
15
|
+
module: Module loading, path conversion, and cross-package module
|
|
16
|
+
discovery utilities.
|
|
17
|
+
package: Package discovery, traversal, and dependency graph analysis.
|
|
18
|
+
|
|
19
|
+
The utilities support both static analysis (without importing) and dynamic
|
|
20
|
+
introspection (with importing), making them suitable for code generation,
|
|
21
|
+
testing frameworks, and package management tools.
|
|
22
|
+
"""
|