Package metametameta

Generate source code metadata for Python projects from existing metadata files.

metametameta

Generate dunder metadata file with __title__, __author__, etc. Also tools to discover these in other packages.

tests pre-commit.ci status Downloads Python Version Release

Installation

pipx install metametameta

Usage

Defaults to putting an __about__.py file in the module directory, assuming your package name is your main module name.

Make best guess what your metadata source is. Give up if there are two possibilities.

metametameta auto 

Run on CI server to see if your about file is out of sync

metametameta sync-check
metametameta poetry # or setup_cfg or pep621 or poetry or importlib or the experimental setup_py

Or set everything explicitly:

metametameta poetry --name "something" --source some.toml --output OUTPUT "mod/meta/__meta__.py"

Subcommand per source.

Usage: metametameta [-h] [--version] [--verbose] [--quiet] {setup_cfg,pep621,poetry,importlib,setup_py,auto,sync-check} ...

metametameta: Generate __about__.py from various sources.

Positional Arguments:
  {setup_cfg,pep621,poetry,importlib,setup_py,auto,sync-check}
                        sub-command help
    setup_cfg           Generate from setup.cfg
    pep621              Generate from PEP 621 pyproject.toml
    poetry              Generate from poetry pyproject.toml
    importlib           Generate from installed package metadata
    setup_py            Generate from setup.py using AST (experimental)
    auto                Automatically detect the source and generate the metadata file.
    sync-check          Check if __about__.py is in sync with the metadata source

Options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  --verbose             verbose output
  --quiet               minimal output

Subcommand help (they all have the same switches)

usage: metametameta poetry [-h] [--name NAME] [--source SOURCE] [--output OUTPUT]

options:
  -h, --help       show this help message and exit
  --name NAME      Name of the project (from file if omitted)
  --source SOURCE  Path to pyproject.toml
  --output OUTPUT  Output file

Programmatic interface.

import metametameta as mmm

mmm.generate_from_pep621()

Motivation

There are many modern ways to get metadata about packages, as of 2024, importlib.metadata and it's backports will get you lots of metadata for yours and other packages.

The newest way is PEP-621, see also packaging.python.org

The oldest way to provide metadata was to use dunder variables in your package, e.g. __author__, __version__, etc.

The method was never strongly standardized, neither officially nor informally. Here is one early proponent of this sort of metadata.

  • Metadata fields can appear in any or no python file in a project.
  • Sometimes they are at the top of a single file python module, common locations for metadata:
    • __init__.py
    • __meta__.py
    • __about__.py
  • Some metadata elements could reasonably be in every single file.
  • There are no particular standards for the type of __author__. It could be a string, space delimited string, list or tuple. That is true for the other metadata elements as well.
  • Sometimes the metadata values are code, e.g. __version__ could be a string or some tuple or data class representing a version.

Workflow

On each build, regenerate the __about__.py. Pick one source of your canonical metadata, e.g. pyproject.toml, setup.py (experimental), setup.cfg.

Using metadata

If you have a lot of packages and you are doing analytics or something with them, you could compile all the metadata as declared in the source code. It could be different from the metadata that shows on the PyPI page. If you are searching for contact info for a package maintainer, this might be useful.

Another marginal use case is in error logging. Error loggers gather up info from just about anywhere, anything can be a clue including metadata of dependencies. So this is one more source of that. See bug_trail for a proof of concept for this usage.

Another marginal use case is that is a folksonomy, a taxonomy created by the people. The official metadata is governed by the Python Packaging Authority and the Python Software Foundation. If, say, you wanted to add a metadata item for __mailing_address__ you could easily do it with source code metadata.

Project Health & Info

Metric Health Metric Info
Tests Tests License License
Coverage Codecov PyPI PyPI
Lint / Pre-commit pre-commit.ci status Python Versions Python Version
Quality Gate Quality Gate Status Docs Docs
CI Build Build Downloads Downloads
Maintainability Maintainability Rating Last Commit Last Commit
Category Health
Open Issues GitHub issues
Stars GitHub Repo stars

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.1.5] - 2024-10-02

Fixed

  • Mypy complaints when lists are empty. Added more null safety checks.

Added

  • metametameta sync-check run on CI server to see if your about file is out of sync
  • metametameta auto make best guess what your metadata source is. Give up if there are two possibilities.

[0.1.4] - 2024-10-02

Fixed

  • Mypy complaints when lists are empty. Added more null safety checks.

Added

  • Experimental setup.py support.

[0.1.3] - 2024-06-28

Fixed

  • Fix bug in pep621 command where - in name but src folder is _

[0.1.2] - 2024-06-14

Fixed

  • Fix bug in pep621 command.

[0.1.1] - 2024-08-03

Added

  • --verbose option

Fixed

  • Less noisy by default, most prints moved to logging invocations

Changed

  • Makes best efforts to find src folder even if different from package name.

[0.1.0] - 2024-01-20

Added

  • Application created.

Sub-modules

metametameta.autodetect

Autodetects the primary source of project metadata.

metametameta.filesystem

This module contains robust functions for writing to the filesystem, intelligently locating the correct Python package directory within a given …

metametameta.find

Find metadata in a module file.

metametameta.find_it
metametameta.from_importlib

Generate an about.py file from package metadata using importlib.metadata.

metametameta.from_pep621

This module contains the function to generate the about.py file from the pyproject.toml file.

metametameta.from_poetry

This module contains the functions to generate the about.py file from the [tool.poetry] section of the pyproject.toml file.

metametameta.from_setup_cfg

This module contains the function to generate the about.py file from the setup.cfg file.

metametameta.from_setup_py

This module contains an experimental function to generate the about.py file by statically parsing a setup.py file using Python's AST module.

metametameta.general

Utilities for generating source code metadata from existing metadata files.

metametameta.known

Known metadata fields, as opposed to ad hoc ones people make up.

metametameta.logging_config

Logging configuration.

metametameta.utils
metametameta.validate_sync

Validation logic to check if about.py is in sync with source metadata.

Functions

def generate_from_importlib(name: str, source: str = '', output: str = '__about__.py', validate: bool = False) ‑> str
Expand source code
def generate_from_importlib(name: str, source: str = "", output: str = "__about__.py", validate: bool = False) -> str:
    """Write package metadata to an __about__.py file."""
    pkg_metadata = get_package_metadata(name)
    if pkg_metadata:
        dir_path = "./"

        about_content, names = any_metadict(pkg_metadata)

        about_content = merge_sections(names, name, about_content)
        file_path = write_to_file(dir_path, about_content, output)
        if validate:
            validate_about_file(file_path, pkg_metadata)
        return file_path
    message = "No [project] section found in pyproject.toml."
    logger.debug(message)
    return message

Write package metadata to an about.py file.

def generate_from_pep621(name: str = '',
source: str = 'pyproject.toml',
output: str = '__about__.py',
validate: bool = False) ‑> str
Expand source code
def generate_from_pep621(name: str = "", source: str = "pyproject.toml", output: str = "__about__.py", validate: bool = False) -> str:
    """
    Generate the __about__.py file from the pyproject.toml file.

    Args:
        validate:
        name (str): Name of the project.
        source (str): Path to the pyproject.toml file.
        output (str): Name of the file to write to.
        validate (bool): Validate file

    Returns:
        str: Path to the file that was written.
    """
    project_data = read_pep621_metadata(source)
    if project_data:
        # Extract the project name and create a directory
        project_name = project_data.get("name", "")
        if not project_name:
            raise TypeError("Project name not found in [project] section of pyproject.toml.")
        if output != "__about__.py" and "/" in output or "\\" in output:
            dir_path = "./"
        else:
            dir_path = f"./{project_name}"

        # if the dir_path does not exist check if project_name.replace("-", "_") exists
        if not Path(dir_path).exists():
            project_name = project_name.replace("-", "_")
            dir_path = f"./{project_name}"

        if not Path(dir_path).exists():
            project_name = project_name.replace("_", "-")
            dir_path = f"./{project_name}"

        result_tuple = None
        try:
            result_tuple = any_metadict(project_data)
            about_content, names = result_tuple
        except Exception:
            print(result_tuple)
            raise
        about_content = merge_sections(names, project_name or "", about_content)
        file_path = write_to_file(dir_path, about_content, output)

        if validate:
            validate_about_file(file_path, project_data)

        return file_path
    logger.debug("No [project] section found in pyproject.toml.")
    return "No [project] section found in pyproject.toml."

Generate the about.py file from the pyproject.toml file.

Args

validate:
name : str
Name of the project.
source : str
Path to the pyproject.toml file.
output : str
Name of the file to write to.
validate : bool
Validate file

Returns

str
Path to the file that was written.
def generate_from_poetry(name: str = '',
source: str = 'pyproject.toml',
output: str = '__about__.py',
validate: bool = True) ‑> str
Expand source code
def generate_from_poetry(name: str = "", source: str = "pyproject.toml", output: str = "__about__.py", validate: bool = True) -> str:
    """
    Generate the __about__.py file from the pyproject.toml file.
    Args:
        name (str): Name of the project.
        source (str): Path to the pyproject.toml file.
        output (str): Name of the file to write to.
        validate (bool): Check if top level values are in about file after written

    Returns:
        str: Path to the file that was written.
    """
    poetry_data = read_poetry_metadata(source)
    if poetry_data:
        candidate_packages = []
        packages_data_list = poetry_data.get("packages")
        if packages_data_list:
            for package_data in packages_data_list:
                include_part = None
                from_part = None  # subfolder(s)
                _format_part = None  # can be dist, i.e not a folder
                for key, value in package_data.items():
                    if key == "include":
                        include_part = value
                    elif key == "from":
                        from_part = value
                    elif key == "format":
                        pass
                candidate_path = ""
                if include_part:
                    candidate_path = include_part
                if include_part and from_part:
                    candidate_from_path = Path(candidate_path) / from_part
                    if candidate_from_path.exists():
                        candidate_path = candidate_from_path
                if Path(candidate_path).exists():
                    candidate_packages.append(candidate_path)

        project_name = poetry_data.get("name")
        if not candidate_packages:
            candidate_packages.append(project_name)
        written = []
        for candidate in candidate_packages:
            if output != "__about__.py" and "/" in output or "\\" in output:
                dir_path = "./"
            else:
                dir_path = f"./{candidate}"
            result_tuple = any_metadict(poetry_data)
            about_content, names = result_tuple
            about_content = merge_sections(names, candidate or "", about_content)
            # Define the content to write to the __about__.py file
            file_path = filesystem.write_to_file(dir_path, about_content, output)

            if validate:
                validate_about_file(file_path, poetry_data)

            written.append(file_path)
    logger.debug("No [tool.poetry] section found in pyproject.toml.")
    return "No [tool.poetry] section found in pyproject.toml."

Generate the about.py file from the pyproject.toml file.

Args

name : str
Name of the project.
source : str
Path to the pyproject.toml file.
output : str
Name of the file to write to.
validate : bool
Check if top level values are in about file after written

Returns

str
Path to the file that was written.
def generate_from_setup_cfg(name: str = '',
source: str = 'setup.cfg',
output: str = '__about__.py',
validate: bool = True) ‑> str
Expand source code
def generate_from_setup_cfg(name: str = "", source: str = "setup.cfg", output: str = "__about__.py", validate: bool = True) -> str:
    """
    Generate the __about__.py file from the setup.cfg file.

    Args:
        name (str): Name of the project.
        source (str): Path to the setup.cfg file.
        output (str): Name of the file to write to.
        validate (bool): Check if top level values are in about file after written

    Returns:
        str: Path to the file that was written.
    """
    metadata = read_setup_cfg_metadata(Path(source))
    if metadata:
        # Directory name
        project_name = metadata.get("name")
        if output != "__about__.py" and "/" in output or "\\" in output:
            dir_path = "./"
        else:
            dir_path = f"./{project_name}"

        # Define the content to write to the __about__.py file
        result_tuple = None
        try:
            result_tuple = any_metadict(metadata)
            about_content, names = result_tuple
        except Exception:
            logger.warning("Can't parse metadata")
            logger.warning(result_tuple)
            raise
        about_content = merge_sections(names, project_name or "", about_content)
        file_path = write_to_file(dir_path, about_content, output)

        if validate:
            validate_about_file(file_path, metadata)

        return file_path
    logger.debug("No [metadata] section found in setup.cfg.")
    return "No [metadata] section found in setup.cfg."

Generate the about.py file from the setup.cfg file.

Args

name : str
Name of the project.
source : str
Path to the setup.cfg file.
output : str
Name of the file to write to.
validate : bool
Check if top level values are in about file after written

Returns

str
Path to the file that was written.
def generate_from_setup_py(name: str = '',
source: str = 'setup.py',
output: str = '__about__.py',
validate: bool = False) ‑> str
Expand source code
def generate_from_setup_py(name: str = "", source: str = "setup.py", output: str = "__about__.py", validate: bool = False) -> str:
    """
    Generate the __about__.py file from a setup.py file.
    """
    metadata = read_setup_py_metadata(source)
    if not metadata:
        message = "No setup() call with static metadata found in setup.py."
        logger.debug(message)
        return message

    # Use the name from the metadata, but allow CLI to override it if provided
    project_name = name or metadata.get("name", "")
    if not project_name:
        raise ValueError("Project 'name' not found in setup.py and not provided via arguments.")

    about_content, names = any_metadict(metadata)
    about_content = merge_sections(names, project_name, about_content)

    file_path = write_to_file(project_name, about_content, output)
    if validate:
        validate_about_file(file_path, metadata)
    return file_path

Generate the about.py file from a setup.py file.