Skip to content

Commit 314d5ba

Browse files
Add test to keep namespace tidy
1 parent bfa3f4f commit 314d5ba

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

tests/test_root_namespace.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright 2024 - present The PyMC Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import types
16+
17+
import pymc
18+
19+
# Submodules whose entire public namespace is re-exported at the pymc root via `from pymc.<submodule> import *`.
20+
_REEXPORTED_SUBMODULES = (
21+
"backends",
22+
"data",
23+
"distributions",
24+
"logprob",
25+
"model.core",
26+
"sampling",
27+
"smc",
28+
"step_methods",
29+
"tuning",
30+
"variational",
31+
)
32+
33+
# Names imported individually into the root namespace from elsewhere. New entries here should be deliberate — adding
34+
# cruft to the root requires updating this list.
35+
_EXPLICIT_ROOT_NAMES = frozenset(
36+
{
37+
"compile", # from pymc.pytensorf
38+
"compute_log_likelihood", # from pymc.stats
39+
"do", # from pymc.model.transform.conditioning
40+
"find_constrained_prior", # from pymc.func_utils
41+
"model_to_graphviz", # from pymc.model_graph
42+
"model_to_mermaid", # from pymc.model_graph
43+
"model_to_networkx", # from pymc.model_graph
44+
"observe", # from pymc.model.transform.conditioning
45+
}
46+
)
47+
48+
49+
def _resolve(dotted):
50+
obj = pymc
51+
for part in dotted.split("."):
52+
obj = getattr(obj, part)
53+
return obj
54+
55+
56+
def test_reexported_submodules_define_all():
57+
"""
58+
Each whitelisted submodule must declare its public surface explicitly via __all__.
59+
60+
Without this guard, removing __all__ from a submodule would silently start re-exporting every non-underscore name
61+
to the pymc root.
62+
"""
63+
missing = [sub for sub in _REEXPORTED_SUBMODULES if not hasattr(_resolve(sub), "__all__")]
64+
assert not missing, f"Submodules missing __all__: {missing}"
65+
66+
67+
def test_root_module_not_polluted():
68+
actual = {
69+
name
70+
for name in dir(pymc)
71+
if not name.startswith("_") and not isinstance(getattr(pymc, name), types.ModuleType)
72+
}
73+
74+
expected = set(_EXPLICIT_ROOT_NAMES)
75+
for sub in _REEXPORTED_SUBMODULES:
76+
expected |= set(_resolve(sub).__all__)
77+
78+
unexpected = actual - expected
79+
missing = expected - actual
80+
hint = (
81+
"If a name is intentional, add it to _EXPLICIT_ROOT_NAMES or to the appropriate submodule's __all__. Otherwise,"
82+
" remove the import from pymc/__init__.py."
83+
)
84+
assert not unexpected, f"Unexpected names at pymc root: {sorted(unexpected)}. {hint}"
85+
assert not missing, f"Missing names at pymc root: {sorted(missing)}. {hint}"

0 commit comments

Comments
 (0)