Skip to content

Commit

Permalink
Significant cleanup x 14.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain quietly resurrecting BETSE
for a modern audience and the modern Python ecosystem. Specifically,
this commit continues resolving failing GitHub Actions-based continuous
integration (CI) workflows by restoring detection of external commands
required by optional runtime dependencies. BETSE 1.4.0 is now still days
from official release, supposedly. (*Blunt runts!*)
  • Loading branch information
leycec committed Sep 23, 2024
1 parent 22cf50b commit 207fcb3
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 89 deletions.
136 changes: 57 additions & 79 deletions betse/lib/libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
# from betse.util.io.log import logs
# from betse.util.app.meta import appmetaone
from betse.util.py.module.pymodname import (
DEPENDENCY_TO_COMMANDS,
DEPENDENCY_TO_MODULE_NAME,
import_module,
is_module,
Expand Down Expand Up @@ -236,38 +237,50 @@ def die_unless_runtime_optional(*dependency_names: str) -> None:
# dependency_command.name,
# dependency_command.basename,
# ))
#
# # ....................{ TESTERS }....................
# @type_check
# def is_command(*dependency_names: str) -> bool:
# '''
# ``True`` only if all external commands required by all application
# dependencies (of any type, including optional, mandatory, runtime, testing,
# or otherwise) with the passed :mod:`setuptools`-specific project names are
# **installed** (i.e., are executable files in the current ``${PATH}``).
#
# Parameters
# ----------
# dependency_names : Tuple[str]
# Tuple of the names of all :mod:`setuptools`-specific projects
# corresponding to these dependencies (e.g., ``NetworkX``).
# '''
#
# # Avoid circular import dependencies.
# from betse.util.os.command import cmdpath
#
# # Return true only if...
# return all(
# # Each external command required by each dependency is in the ${PATH}.
# cmdpath.is_pathable(dependency_command.basename)
# # For the name of each passed dependency...
# for requirement_name in dependency_names
# # For each "RequirementCommand" instance describing an external command
# # required by this dependency...
# for dependency_command in _iter_requirement_commands(requirement_name)
# )
#
#

# ....................{ TESTERS }....................
def is_commands(*dependency_names: str) -> bool:
'''
``True`` only if all external commands required by all application
dependencies (of any type, including optional, mandatory, runtime, testing,
or otherwise) with the passed :mod:`setuptools`-specific project names are
**installed** (i.e., are executable files in the current ``${PATH}``).
Parameters
----------
dependency_names : Tuple[str]
Tuple of the names of all :mod:`setuptools`-specific projects
corresponding to these dependencies (e.g., ``NetworkX``).
'''

# Avoid circular import dependencies.
from betse.util.os.command.cmdpath import is_pathable

# Return true only if...
return all(
# Each external command required by each dependency is in the ${PATH}.
is_pathable(dependency_command.basename)
# For the name of each passed dependency...
for dependency_name in dependency_names
# For each "RequirementCommand" instance describing an external command
# required by this dependency...
for dependency_command in DEPENDENCY_TO_COMMANDS.get(
dependency_name, ())
)

#FIXME: Resurrect this when refactoring to "importlib.metadata", please.
# # Return true only if...
# return all(
# # Each external command required by each dependency is in the ${PATH}.
# cmdpath.is_pathable(dependency_command.basename)
# # For the name of each passed dependency...
# for requirement_name in dependency_names
# # For each "RequirementCommand" instance describing an external command
# # required by this dependency...
# for dependency_command in _iter_requirement_commands(requirement_name)
# )


def is_runtime_optional(*dependency_names: str) -> bool:
'''
``True`` only if all optional runtime dependencies of this application with
Expand Down Expand Up @@ -302,11 +315,19 @@ def is_runtime_optional(*dependency_names: str) -> bool:
module_name = DEPENDENCY_TO_MODULE_NAME.get(
dependency_name, dependency_name)

# If this dependency is unimportable, immediately return false.
if not is_module(module_name):
# If it is *NOT* the case that...
if not (
# This dependency is importable *AND*...
is_module(module_name) and
# All external commands required by this dependency are installed in
# the current "${PATH}"...
is_commands(dependency_name)
# Then immediately return false.
):
return False
# Else, this dependency is importable. Silently continue to the next.
# Else, all dependencies are importable.
# Else, this dependency is importable *AND* all external commands
# required by this dependency are installed in the current "${PATH}".
# Silently continue to the next dependency.

# Return true.
return True
Expand Down Expand Up @@ -495,46 +516,3 @@ def import_runtime_optional(*dependency_names: str) -> ModuleOrSequenceTypes:
# # Validate all external commands required by these dependencies.
# return setuptool.import_requirements_dict_keys(
# requirements_dict, *dependency_names)
#
# # ....................{ PRIVATE ~ iterators }....................
# @type_check
# def _iter_requirement_commands(requirement_name: str) -> SequenceTypes:
# '''
# Sequence of zero or more ``RequirementCommand`` instances describing all
# external commands required by the application dependency with the passed
# :mod:`setuptools`-specific project name.
#
# Parameters
# ----------
# requirement_name : str
# Name of the :mod:`setuptools`-specific project to be inspected.
#
# Returns
# ----------
# SequenceTypes:
# Sequence of zero or more ``RequirementCommand`` instances describing all
# external commands required by this project.
# '''
#
# # Application-wide dependency metadata submodule.
# metadeps = appmetaone.get_app_meta().module_metadeps
#
# # Tuple of zero or more "RequirementCommand" instances describing
# # each external command required by this dependency if any *OR* the
# # empty tuple otherwise.
# dependency_commands = metadeps.REQUIREMENT_NAME_TO_COMMANDS.get(
# requirement_name, ())
#
# #FIXME: Additionally validate that each "namedtuple" instance provides the
# #"name" and "basename" fields.
# # Validate this tuple to contain only "namedtuple" instances.
# #
# # Note that we intentionally do *NOT* validate this tuple to contain only
# # "betse.metadata.RequirementCommand" instances, as downstream consumers
# # (e.g., BETSEE) necessarily duplicate that type into their own codebases
# # (e.g., as "betsee.guimetadata.RequirementCommand").
# itertest.die_unless_items_instance_of(
# iterable=dependency_commands, cls=tuple)
#
# # Return this tuple.
# return dependency_commands
12 changes: 7 additions & 5 deletions betse/util/io/error/errwarning.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@

# ....................{ IMPORTS }....................
import sys, warnings
from beartype.typing import ContextManager
from beartype.typing import (
ContextManager,
Generator,
)
from betse.util.io.log import logs
from betse.util.type.types import type_check, ClassType, GeneratorType
from betse.util.type.types import ClassType
from contextlib import contextmanager

# ....................{ INITIALIZERS }....................
Expand Down Expand Up @@ -79,7 +82,7 @@ def init() -> None:
logs.log_debug('Deferring to default warning policy.')

# ....................{ MANAGERS }....................
def ignoring_deprecations() -> GeneratorType:
def ignoring_deprecations() -> ContextManager:
'''
Single-shot context manager temporarily ignoring all **deprecation
warnings** (i.e., instances of the :class:`DeprecationWarning`,
Expand All @@ -101,8 +104,7 @@ def ignoring_deprecations() -> GeneratorType:


@contextmanager
@type_check
def ignoring_warnings(*warning_clses: ClassType) -> ContextManager:
def ignoring_warnings(*warning_clses: ClassType) -> Generator:
'''
Single-shot context manager temporarily ignoring *all* warnings of *all*
passed warning types emitted by the :mod:`warnings` module for the duration
Expand Down
49 changes: 44 additions & 5 deletions betse/util/py/module/pymodname.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,55 @@
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

import importlib, sys
from beartype.typing import (
Collection,
Dict,
)
from betse.exceptions import BetseModuleException
from betse.util.io.log import logs
from betse.util.type.iterable.mapping.mapcls import DefaultDict
from betse.util.type.types import (
type_check, MappingOrNoneTypes, ModuleType, StrOrNoneTypes)
from collections import defaultdict
from collections import (
defaultdict,
namedtuple,
)
from importlib import util as importlib_util
from importlib.machinery import ModuleSpec

# ....................{ CLASSES }....................
DependencyCommand = namedtuple('DependencyCommand', ('name', 'basename',))
DependencyCommand.__doc__ = '''
Lightweight metadata describing a single external command required by an
application dependency of arbitrary type (including optional, mandatory,
runtime, testing, and otherwise).
Attributes
----------
name : str
Human-readable name associated with this command (e.g., ``Graphviz``).
basename : str
Basename of this command to be searched for in the current ``${PATH}``.
'''

# ....................{ GLOBALS }....................
DEPENDENCY_TO_COMMANDS: Dict[str, Collection[DependencyCommand]] = {
# "pydot" requires Graphviz (i.e., "dot") to be externally installed.
'pydot': (DependencyCommand(name='Graphviz', basename='dot'),),
}
'''
Dictionary mapping each **runtime dependency name** (i.e., human-readable name
of an open-source project hosted by the Python Packaging Index (PyPI), such as
``"PyYAML"``, of a runtime dependency of this project) to a collection of one
or more external commands required by that project.
See Also
--------
:download:`/doc/md/INSTALL.md`
Human-readable list of these dependencies.
'''


DEPENDENCY_TO_MODULE_NAME = DefaultDict(
missing_key_value=lambda self, missing_key: missing_key,
initial_mapping={
Expand All @@ -46,10 +85,10 @@
}
)
'''
Dictionary mapping the human-readable name of an open-source project hosted by
the Python Packaging Index (PyPI) (e.g., ``PyYAML``) to the corresponding
fully-qualified name of the corresponding module or package providing that
project (e.g., ``yaml``).
Dictionary mapping each **runtime dependency name** (i.e., human-readable name
of an open-source project hosted by the Python Packaging Index (PyPI), such as
``"PyYAML"``, of a runtime dependency of this project) to the fully-qualified
name of the module or package providing that project (e.g., ``yaml``).
All modules and packages explicitly unmapped by this dictionary default to the
identity mapping -- that is, mapping the fully-qualified names of those modules
Expand Down

0 comments on commit 207fcb3

Please sign in to comment.