File: utilmodtest.py

package info (click to toggle)
python-beartype 0.22.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,504 kB
  • sloc: python: 85,502; sh: 328; makefile: 30; javascript: 18
file content (252 lines) | stat: -rw-r--r-- 9,564 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **Python module tester** (i.e., callables dynamically testing
modules and/or attributes in modules) utilities.

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                            }....................
from beartype.roar._roarexc import _BeartypeUtilModuleException
from beartype.typing import Optional
from beartype._cave._cavefast import ModuleType
from beartype._data.typing.datatyping import TypeException
from beartype._util.error.utilerrwarn import warnings_ignored
from beartype._util.text.utiltextidentifier import die_unless_identifier
from beartype._util.text.utiltextversion import convert_str_version_to_tuple
from importlib.metadata import version as get_module_version  # type: ignore[attr-defined]

# ....................{ RAISERS                            }....................
#FIXME: Excise us up. This function is no longer called anywhere. *sigh*
def die_unless_module_attr_name(
    # Mandatory parameters.
    module_attr_name: str,

    # Optional parameters.
    exception_cls: TypeException = _BeartypeUtilModuleException,
    exception_prefix: str = 'Module attribute name ',
) -> None:
    '''
    Raise an exception unless the passed string is the fully-qualified
    syntactically valid name of a **module attribute** (i.e., object declared
    at module scope by a module) that may or may not actually exist.

    This validator does *not* validate this attribute to actually exist -- only
    that the name of this attribute is syntactically valid.

    Parameters
    ----------
    module_attr_name : str
        Fully-qualified name of the module attribute to be validated.
    exception_cls : type, optional
        Type of exception to be raised in the event of a fatal error. Defaults
        to :class:`._BeartypeUtilModuleException`.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to something reasonably sane.

    Raises
    ------
    exception_cls
        If either:

        * This name is *not* a string.
        * This name is a string containing either:

          * *No* ``.`` characters and thus either:

            * Is relative to the calling subpackage and thus *not*
              fully-qualified (e.g., ``muh_submodule``).
            * Refers to a builtin object (e.g., ``str``). While technically
              fully-qualified, the names of builtin objects are *not*
              explicitly importable as is. Since these builtin objects are
              implicitly imported everywhere, there exists *no* demonstrable
              reason to even attempt to import them anywhere.

          * One or more ``.`` characters but syntactically invalid as an
            identifier (e.g., ``0h!muh?G0d.``).
    '''
    assert isinstance(exception_cls, type), f'{repr(exception_cls)} not type.'
    assert isinstance(exception_prefix, str), (
        f'{repr(exception_prefix)} not string.')

    # If this object is *NOT* a string, raise an exception.
    if not isinstance(module_attr_name, str):
        raise exception_cls(
            f'{exception_prefix}{repr(module_attr_name)} not string.')
    # Else, this object is a string.
    #
    # If this string contains *NO* "." characters and thus either is relative to
    # the calling subpackage or refers to a builtin object, raise an exception.
    elif '.' not in module_attr_name:
        raise exception_cls(
            f'{exception_prefix}"{module_attr_name}" '
            f'relative or refers to builtin object '
            f'(i.e., due to containing no "." characters).'
        )
    # Else, this string contains one or more "." characters and is thus the
    # fully-qualified name of a non-builtin type.
    #
    # If this string is syntactically invalid as a fully-qualified module
    # attribute name, raise an exception.
    else:
        die_unless_identifier(
            text=module_attr_name,
            exception_cls=exception_cls,
            exception_prefix=exception_prefix,
        )
    # Else, this string is syntactically valid as a fully-qualified module
    # attribute name.

# ....................{ TESTERS                            }....................
def is_module(
    # Mandatory parameters.
    module_name: str,

    # Optional parameters.
    is_warnings_ignore: bool = False,
) -> bool:
    '''
    :data:`True` only if the module or C extension with the passed
    fully-qualified name is importable under the active Python interpreter.

    Caveats
    -------
    **This tester dynamically imports this module as an unavoidable side effect
    of performing this test.**

    Parameters
    ----------
    module_name : str
        Fully-qualified name of the module to be imported.
    is_warnings_ignore : bool, optional
        :data:`True` only if this tester ignores *all* warnings transitively
        emitted as a side effect by the importation of this module. Defaults to
        :data:`False` for safety.

    Returns
    -------
    bool
        :data:`True` only if this module is importable.

    Warns
    -----
    BeartypeModuleUnimportableWarning
        If a module with this name exists *but* that module is unimportable due
        to raising module-scoped exceptions at importation time.
    '''

    # Avoid circular import dependencies.
    from beartype._util.module.utilmodimport import import_module_or_none

    # Module with this name if this module is importable *OR* "None" otherwise.
    module: Optional[ModuleType] = None

    # If ignoring *ALL* warnings transitively emitted as a side effect by the
    # importation of this module, attempt to dynamically import this module
    # under a context manager ignoring these warnings.
    if is_warnings_ignore:
        with warnings_ignored():
            module = import_module_or_none(module_name)
    # Else, dynamically import this module *WITHOUT* ignoring these warnings.
    else:
        module = import_module_or_none(module_name)

    # Return true only if this module is importable.
    return module is not None


#FIXME: Unit test us up against "setuptools", the only third-party package
#*BASICALLY* guaranteed to be importable.
def is_module_version_at_least(module_name: str, version_minimum: str) -> bool:
    '''
    :data:`True` only if the module or C extension with the passed
    fully-qualified name is both importable under the active Python interpreter
    *and* at least as new as the passed version.

    Caveats
    -------
    **This tester dynamically imports this module as an unavoidable side effect
    of performing this test.**

    Parameters
    ----------
    module_name : str
        Fully-qualified name of the module to be imported.
    version_minimum : str
        Minimum version to test this module against as a dot-delimited
        :pep:`440`-compliant version specifier (e.g., ``42.42.42rc42.post42``).

    Returns
    -------
    bool
        :data:`True` only if:

        * This module is importable.
        * This module's version is at least the passed version.

    Warns
    -----
    BeartypeModuleUnimportableWarning
        If a module with this name exists *but* that module is unimportable due
        to raising module-scoped exceptions at importation time.
    '''
    assert isinstance(version_minimum, str), (
        f'{repr(version_minimum)} not string.')

    # If this module is unimportable, return false immediately.
    if not is_module(module_name):
        return False
    # Else, this module is importable.

    # Current version of this module installed under the active Python
    # interpreter if any *OR* raise an exception otherwise (which should
    # *NEVER* happen by prior logic testing this module to be importable).
    version_actual = get_module_version(module_name)

    # Tuples of version parts parsed from version strings.
    version_actual_parts  = convert_str_version_to_tuple(version_actual)
    version_minimum_parts = convert_str_version_to_tuple(version_minimum)

    # Return true only if this module's version satisfies this minimum.
    return version_actual_parts >= version_minimum_parts

# ....................{ TESTERS ~ package                  }....................
#FIXME: Unit test us up, please.
def is_package(package_name: str, **kwargs) -> bool:
    '''
    :data:`True` only if the package with the passed fully-qualified name is
    importable under the active Python interpreter.

    Caveats
    -------
    **This tester dynamically imports this module as an unavoidable side effect
    of performing this test.**

    Parameters
    ----------
    package_name : str
        Fully-qualified name of the package to be imported.

    All remaining keyword parameters are passed as is to the lower-level
    :func:`.is_module` tester.

    Returns
    -------
    bool
        :data:`True` only if this package is importable.

    Warns
    -----
    BeartypeModuleUnimportableWarning
        If a package with this name exists *but* that package is unimportable
        due to raising module-scoped exceptions from the top-level ``__init__``
        submodule of this package at importation time.
    '''

    # Be the one liner you want to see in the world.
    return is_module(f'{package_name}.__init__', **kwargs)