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.
Files changed (102) hide show
  1. pyrig/__init__.py +1 -0
  2. pyrig/dev/__init__.py +6 -0
  3. pyrig/dev/builders/__init__.py +1 -0
  4. pyrig/dev/builders/base/__init__.py +5 -0
  5. pyrig/dev/builders/base/base.py +256 -0
  6. pyrig/dev/builders/pyinstaller.py +229 -0
  7. pyrig/dev/cli/__init__.py +5 -0
  8. pyrig/dev/cli/cli.py +95 -0
  9. pyrig/dev/cli/commands/__init__.py +1 -0
  10. pyrig/dev/cli/commands/build_artifacts.py +16 -0
  11. pyrig/dev/cli/commands/create_root.py +25 -0
  12. pyrig/dev/cli/commands/create_tests.py +244 -0
  13. pyrig/dev/cli/commands/init_project.py +160 -0
  14. pyrig/dev/cli/commands/make_inits.py +27 -0
  15. pyrig/dev/cli/commands/protect_repo.py +145 -0
  16. pyrig/dev/cli/shared_subcommands.py +20 -0
  17. pyrig/dev/cli/subcommands.py +73 -0
  18. pyrig/dev/configs/__init__.py +1 -0
  19. pyrig/dev/configs/base/__init__.py +5 -0
  20. pyrig/dev/configs/base/base.py +826 -0
  21. pyrig/dev/configs/containers/__init__.py +1 -0
  22. pyrig/dev/configs/containers/container_file.py +111 -0
  23. pyrig/dev/configs/dot_env.py +95 -0
  24. pyrig/dev/configs/dot_python_version.py +88 -0
  25. pyrig/dev/configs/git/__init__.py +5 -0
  26. pyrig/dev/configs/git/gitignore.py +181 -0
  27. pyrig/dev/configs/git/pre_commit.py +170 -0
  28. pyrig/dev/configs/licence.py +112 -0
  29. pyrig/dev/configs/markdown/__init__.py +1 -0
  30. pyrig/dev/configs/markdown/docs/__init__.py +1 -0
  31. pyrig/dev/configs/markdown/docs/index.py +38 -0
  32. pyrig/dev/configs/markdown/readme.py +132 -0
  33. pyrig/dev/configs/py_typed.py +28 -0
  34. pyrig/dev/configs/pyproject.py +436 -0
  35. pyrig/dev/configs/python/__init__.py +5 -0
  36. pyrig/dev/configs/python/builders_init.py +27 -0
  37. pyrig/dev/configs/python/configs_init.py +28 -0
  38. pyrig/dev/configs/python/dot_experiment.py +46 -0
  39. pyrig/dev/configs/python/main.py +59 -0
  40. pyrig/dev/configs/python/resources_init.py +27 -0
  41. pyrig/dev/configs/python/shared_subcommands.py +29 -0
  42. pyrig/dev/configs/python/src_init.py +27 -0
  43. pyrig/dev/configs/python/subcommands.py +27 -0
  44. pyrig/dev/configs/testing/__init__.py +5 -0
  45. pyrig/dev/configs/testing/conftest.py +64 -0
  46. pyrig/dev/configs/testing/fixtures_init.py +27 -0
  47. pyrig/dev/configs/testing/main_test.py +74 -0
  48. pyrig/dev/configs/testing/zero_test.py +43 -0
  49. pyrig/dev/configs/workflows/__init__.py +5 -0
  50. pyrig/dev/configs/workflows/base/__init__.py +5 -0
  51. pyrig/dev/configs/workflows/base/base.py +1662 -0
  52. pyrig/dev/configs/workflows/build.py +106 -0
  53. pyrig/dev/configs/workflows/health_check.py +133 -0
  54. pyrig/dev/configs/workflows/publish.py +68 -0
  55. pyrig/dev/configs/workflows/release.py +90 -0
  56. pyrig/dev/tests/__init__.py +5 -0
  57. pyrig/dev/tests/conftest.py +40 -0
  58. pyrig/dev/tests/fixtures/__init__.py +1 -0
  59. pyrig/dev/tests/fixtures/assertions.py +147 -0
  60. pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
  61. pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
  62. pyrig/dev/tests/fixtures/autouse/module.py +40 -0
  63. pyrig/dev/tests/fixtures/autouse/session.py +589 -0
  64. pyrig/dev/tests/fixtures/factories.py +118 -0
  65. pyrig/dev/utils/__init__.py +1 -0
  66. pyrig/dev/utils/cli.py +17 -0
  67. pyrig/dev/utils/git.py +312 -0
  68. pyrig/dev/utils/packages.py +93 -0
  69. pyrig/dev/utils/resources.py +77 -0
  70. pyrig/dev/utils/testing.py +66 -0
  71. pyrig/dev/utils/versions.py +268 -0
  72. pyrig/main.py +9 -0
  73. pyrig/py.typed +0 -0
  74. pyrig/resources/GITIGNORE +216 -0
  75. pyrig/resources/LATEST_PYTHON_VERSION +1 -0
  76. pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
  77. pyrig/resources/__init__.py +1 -0
  78. pyrig/src/__init__.py +1 -0
  79. pyrig/src/git/__init__.py +6 -0
  80. pyrig/src/git/git.py +146 -0
  81. pyrig/src/graph.py +255 -0
  82. pyrig/src/iterate.py +107 -0
  83. pyrig/src/modules/__init__.py +22 -0
  84. pyrig/src/modules/class_.py +369 -0
  85. pyrig/src/modules/function.py +189 -0
  86. pyrig/src/modules/inspection.py +148 -0
  87. pyrig/src/modules/module.py +658 -0
  88. pyrig/src/modules/package.py +452 -0
  89. pyrig/src/os/__init__.py +6 -0
  90. pyrig/src/os/os.py +121 -0
  91. pyrig/src/project/__init__.py +5 -0
  92. pyrig/src/project/mgt.py +83 -0
  93. pyrig/src/resource.py +58 -0
  94. pyrig/src/string.py +100 -0
  95. pyrig/src/testing/__init__.py +6 -0
  96. pyrig/src/testing/assertions.py +66 -0
  97. pyrig/src/testing/convention.py +203 -0
  98. pyrig-2.2.6.dist-info/METADATA +174 -0
  99. pyrig-2.2.6.dist-info/RECORD +102 -0
  100. pyrig-2.2.6.dist-info/WHEEL +4 -0
  101. pyrig-2.2.6.dist-info/entry_points.txt +3 -0
  102. 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
+ """