Skip to content

Commit 48053df

Browse files
Peter554seddonym
authored andcommitted
Add find_matching_modules and find_matching_direct_imports
1 parent 8c1bb5d commit 48053df

19 files changed

Lines changed: 870 additions & 18 deletions

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
Unreleased
66
----------
77

8+
* Added `find_matching_modules` and `find_matching_direct_imports` methods.
89
* Added `as_packages` option to the `find_shortest_chain` method.
910

1011
3.6 (2025-02-07)

docs/usage.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ Methods for analysing the module tree
121121
:raises: ``ValueError`` if the module is a squashed module, as by definition it represents both itself and all
122122
of its descendants.
123123

124+
.. py:function:: ImportGraph.find_matching_modules(expression)
125+
126+
Find all modules matching the passed expression (see :ref:`module_expressions`).
127+
128+
:param str expression: A module expression used for matching.
129+
:return: A set of module names matching the expression.
130+
:rtype: A set of strings.
131+
:raises: ``grimp.exceptions.InvalidModuleExpression`` if the module expression is invalid.
132+
133+
124134
Methods for analysing direct imports
125135
------------------------------------
126136

@@ -184,6 +194,28 @@ Methods for analysing direct imports
184194
So if a module is imported twice from the same module, it will only be counted once.
185195
:rtype: Integer.
186196

197+
.. py:function:: ImportGraph.find_matching_direct_imports(import_expression)
198+
199+
Find all direct imports matching the passed import expression.
200+
201+
The imports are returned are in the following form::
202+
203+
[
204+
{
205+
'importer': 'mypackage.importer',
206+
'imported': 'mypackage.imported',
207+
},
208+
# (additional imports here)
209+
]
210+
211+
:param str import_expression: An expression in the form ``"importer_expression -> imported_expression"``,
212+
where each expression is a module expression (see :ref:`module_expressions`).
213+
Example: ``"mypackage.*.blue -> mypackage.*.green"``.
214+
:return: An ordered list of direct imports matching the expressions (ordered alphabetically).
215+
:rtype: List of dictionaries with the structure shown above. If you want to use type annotations, you may use the
216+
``grimp.Import`` TypedDict for each dictionary.
217+
:raises: ``grimp.exceptions.InvalidImportExpression`` if the expression is not well-formed.
218+
187219
Methods for analysing import chains
188220
-----------------------------------
189221

@@ -517,5 +549,25 @@ Methods for manipulating the graph
517549
:param str module: The name of a module, for example ``'mypackage.foo'``.
518550
:return: bool
519551

552+
.. _module_expressions:
553+
554+
Module expressions
555+
------------------
556+
557+
A module expression is used to refer to sets of modules.
558+
559+
- ``*`` stands in for a module name, without including subpackages.
560+
- ``**`` includes subpackages too.
561+
562+
Examples:
563+
564+
- ``mypackage.foo``: matches ``mypackage.foo`` exactly.
565+
- ``mypackage.*``: matches ``mypackage.foo`` but not ``mypackage.foo.bar``.
566+
- ``mypackage.*.baz``: matches ``mypackage.foo.baz`` but not ``mypackage.foo.bar.baz``.
567+
- ``mypackage.*.*``: matches ``mypackage.foo.bar`` and ``mypackage.foobar.baz``.
568+
- ``mypackage.**``: matches ``mypackage.foo.bar`` and ``mypackage.foo.bar.baz``.
569+
- ``mypackage.**.qux``: matches ``mypackage.foo.bar.qux`` and ``mypackage.foo.bar.baz.qux``.
570+
- ``mypackage.foo*``: is not a valid expression. (The wildcard must replace a whole module name.)
571+
520572
.. _namespace packages: https://docs.python.org/3/glossary.html#term-namespace-package
521573
.. _namespace portion: https://docs.python.org/3/glossary.html#term-portion

rust/Cargo.lock

Lines changed: 107 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ itertools = "0.14.0"
2020
tap = "1.0.1"
2121
rustc-hash = "2.1.0"
2222
indexmap = "2.7.1"
23+
regex = "1.11.1"
24+
const_format = "0.2.34"
2325

2426
[dependencies.pyo3]
2527
version = "0.23.4"
@@ -29,4 +31,5 @@ extension-module = ["pyo3/extension-module"]
2931
default = ["extension-module"]
3032

3133
[dev-dependencies]
34+
parameterized = "2.0.0"
3235
serde_json = "1.0.137"

rust/src/errors.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::exceptions::{ModuleNotPresent, NoSuchContainer};
1+
use crate::exceptions::{InvalidModuleExpression, ModuleNotPresent, NoSuchContainer};
22
use pyo3::exceptions::PyValueError;
33
use pyo3::PyErr;
44
use thiserror::Error;
@@ -13,6 +13,9 @@ pub enum GrimpError {
1313

1414
#[error("Modules have shared descendants.")]
1515
SharedDescendants,
16+
17+
#[error("{0} is not a valid module expression.")]
18+
InvalidModuleExpression(String),
1619
}
1720

1821
pub type GrimpResult<T> = Result<T, GrimpError>;
@@ -24,6 +27,9 @@ impl From<GrimpError> for PyErr {
2427
GrimpError::ModuleNotPresent(_) => ModuleNotPresent::new_err(value.to_string()),
2528
GrimpError::NoSuchContainer(_) => NoSuchContainer::new_err(value.to_string()),
2629
GrimpError::SharedDescendants => PyValueError::new_err(value.to_string()),
30+
GrimpError::InvalidModuleExpression(_) => {
31+
InvalidModuleExpression::new_err(value.to_string())
32+
}
2733
}
2834
}
2935
}

rust/src/exceptions.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ use pyo3::create_exception;
22

33
create_exception!(_rustgrimp, ModuleNotPresent, pyo3::exceptions::PyException);
44
create_exception!(_rustgrimp, NoSuchContainer, pyo3::exceptions::PyException);
5+
create_exception!(
6+
_rustgrimp,
7+
InvalidModuleExpression,
8+
pyo3::exceptions::PyException
9+
);

0 commit comments

Comments
 (0)