Skip to content

Types and Helpers

Collection of shared structures, types, and functions.

Attributes

ImportKind: TypeAlias = Literal['dynamic', 'conditional', 'typing', 'parent', 'vanilla'] module-attribute

The types/kinds of imports that are currently recognized.

EdgeKind: TypeAlias = Literal['good', 'bad', 'complicated', 'skip'] module-attribute

Describes an import cycle, helping with evaluating how much of an issue it could be.

GraphDict: TypeAlias = dict[str, dict[str, ImportMetaData]] module-attribute

Dictionary representation of an import graph.

The keys on the first level are the qualnames of importing modules, the keys on the second level are the qualnames of imported modules, and the values on the second level contain the metadata dictionary of their keyed imports (ImportMetaData). Metadata consists of a tags-list of ImportKinds and a cycle which is either an EdgeKind if there is a cycle, or None if there isn't.

Example
{
    "foo": {
        "foo.a": {
            "tags": ["vanilla"],
            "cycle": "complicated"
        }
    }
    "foo.a": {
        "foo": {
            "tags": ["parent", "vanilla"],
            "cycle": "complicated"
        },
        "foo.c": {
            "tags": ["vanilla"],
            "cycle": None
        }
    }
    "foo.b": {
        "foo": {
            "tags": ["parent"],
            "cycle": "complicated"
        },
        "foo.c": {
            "tags": ["typing"],
            "cycle": "skip"
        }
    }
    "foo.c": {
        "foo": {
            "tags": ["parent"],
            "cycle": None
        },
        "foo.b": {
            "tags": ["vanilla"],
            "cycle": "skip"
        }
    }
}

Classes

ImportMetaData

Bases: TypedDict

Metadata to interpret import cycle severity.

Source code in src/byecycle/misc.py
class ImportMetaData(TypedDict):
    """Metadata to interpret import cycle severity."""

    tags: list[ImportKind]
    cycle: EdgeKind | None

SeverityMap

Bases: TypedDict

Mapping of ImportKinds to EdgeKinds.

Source code in src/byecycle/misc.py
class SeverityMap(TypedDict, total=False):
    """Mapping of [`ImportKind`][byecycle.misc.ImportKind]s to [`EdgeKind`][byecycle.misc.ImportKind]s."""

    dynamic: EdgeKind
    conditional: EdgeKind
    typing: EdgeKind
    parent: EdgeKind
    vanilla: EdgeKind

ImportStatement

Bases: TypedDict

Container for the information that we need from an AST import node.

The module-value is always non-empty, the name-value is only set by ast.ImportFrom.

Source code in src/byecycle/misc.py
class ImportStatement(TypedDict):
    """Container for the information that we need from an AST import node.

    The module-value is always non-empty, the name-value is only set by `ast.ImportFrom`.
    """

    module: str
    name: str | None

Functions

cycle_severity(tags_a, tags_b, **kwargs)

Interpret the severity of an import cycle given their tags.

In general, all tags get thrown in the same bag and the one with the highest mapped severity "wins". Except for the "vanilla" tag, which will only have its severity considered if both imports had "vanilla" in their tag list.

Parameters:

  • tags_a (set[ImportKind]) –

    The set of import-kind tags for the first import statement.

  • tags_b (set[ImportKind]) –

    The set of import-kind tags for the second import statement.

  • **kwargs (Unpack[SeverityMap]) –

    Valid values are keywords equating to ImportKinds mapping to EdgeKinds in order to override that ImportKind's severity-interpretation.

Returns:

  • EdgeKind

    A string denoting the severity of the cycle.

Source code in src/byecycle/misc.py
def cycle_severity(
    tags_a: set[ImportKind], tags_b: set[ImportKind], **kwargs: Unpack[SeverityMap]
) -> EdgeKind:
    """Interpret the severity of an import cycle given their tags.

    In general, all tags get thrown in the same bag and the one with the highest mapped
    severity "wins". Except for the "vanilla" tag, which will only have its severity
    considered if both imports had "vanilla" in their tag list.

    Args:
        tags_a: The set of import-kind tags for the first import statement.
        tags_b: The set of import-kind tags for the second import statement.
        **kwargs: Valid values are keywords equating to [`ImportKind`][byecycle.misc.ImportKind]s
            mapping to [`EdgeKind`][byecycle.misc.ImportKind]s in order to override that
            [`ImportKind`][byecycle.misc.ImportKind]'s severity-interpretation.

    Returns:
        A string denoting the severity of the cycle.
    """
    tags: set[ImportKind] = tags_a | tags_b
    if "vanilla" in tags and ("vanilla" not in tags_a or "vanilla" not in tags_b):
        tags.remove("vanilla")
    severity_map = cast(SeverityMap, {**_default_cycle_severity, **kwargs})
    severity = sorted((severity_map[t] for t in tags), key=edge_order.get)  # type: ignore[arg-type]
    return severity[0]

path_to_module_name(path, base, name)

Turns a file path into a valid module name.

Just an educated guess on my part, I couldn't find an official reference. Chances are that you can't know the real name of a package unless you actually install it, and only projects that adhere to best practices and/or common sense regarding naming and structure can be handled correctly by this function.

Parameters:

  • path (str) –

    Absolute path to the file in question.

  • base (str) –

    Absolute path to the "source root".

  • name (str) –

    Name of the distribution.

Returns:

  • str

    A string in the form of an importable python module.

Example
>>> # Sample call #1:
>>> path_to_module_name(
...     path = "/home/me/dev/project/src/project/__init__.py"
...     base = "/home/me/dev/project/src/project/"
...     name = "project"
... )
project
>>> # Sample call #2:
>>> path_to_module_name(
...     path = "/home/me/dev/project/src/project/code.py"
...     base = "/home/me/dev/project/src/project/"
...     name = "project"
... )
project.code
>>> # etc.
Notes
  • What if there is a namespace?
  • What if the name of the distribution is different from the base?
  • What if a distribution installs multiple packages?
Source code in src/byecycle/misc.py
def path_to_module_name(path: str, base: str, name: str) -> str:
    """Turns a file path into a valid module name.

    Just an educated guess on my part, I couldn't find an official reference. Chances are
    that you can't know the real name of a package unless you actually install it, and
    only projects that adhere to best practices and/or common sense regarding naming and
    structure can be handled correctly by this function.

    Args:
        path: Absolute path to the file in question.
        base: Absolute path to the "source root".
        name: Name of the distribution.

    Returns:
        A string in the form of an importable python module.

    Example:
        ```py
        >>> # Sample call #1:
        >>> path_to_module_name(
        ...     path = "/home/me/dev/project/src/project/__init__.py"
        ...     base = "/home/me/dev/project/src/project/"
        ...     name = "project"
        ... )
        project
        >>> # Sample call #2:
        >>> path_to_module_name(
        ...     path = "/home/me/dev/project/src/project/code.py"
        ...     base = "/home/me/dev/project/src/project/"
        ...     name = "project"
        ... )
        project.code
        >>> # etc.
        ```

    Notes:
        - What if there is a namespace?
        - What if the name of the distribution is different from the base?
        - What if a distribution installs multiple packages?
    """
    return (
        path.removeprefix(base[: -len(name)])
        .removesuffix(".py")
        .removesuffix("__init__")
        .strip("/")
        .replace("/", ".")
    )