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.

Installation

pipx install metametameta

Usage

metametameta --source pyproject.toml --target mypackage/__metadata__.py

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, 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.

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.0] - 2024-01-20

Added

  • Application created.
Expand source code
"""
Generate source code metadata for Python projects from existing metadata files.

.. include:: ../README.md

.. include:: ../CHANGELOG.md
"""

# All generate functions have the same signature:
# def _(name: str, source:str, output: str) -> None:

__all__ = ["generate_from_setup_cfg", "generate_from_pep621", "generate_from_poetry", "generate_from_importlib"]

from metametameta.from_importlib import generate_from_importlib
from metametameta.from_pep621 import generate_from_pep621
from metametameta.from_poetry import generate_from_poetry
from metametameta.from_setup_cfg import generate_from_setup_cfg

Sub-modules

metametameta.filesystem

This module contains functions for working with the filesystem.

metametameta.find

Find metadata in a module file.

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.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.

Functions

def generate_from_importlib(name: str, source: str = '', output: str = '__about__.py') ‑> str

Write package metadata to an about.py file.

Expand source code
def generate_from_importlib(name: str, source: str = "", output: str = "__about__.py") -> 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 = general.merge_sections(names, name, about_content)
        return write_to_file(dir_path, about_content, output)
    else:
        return "No [project] section found in pyproject.toml."
def generate_from_pep621(name: str = '', source: str = 'pyproject.toml', output: str = '__about__.py') ‑> 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.

Returns

str
Path to the file that was written.
Expand source code
def generate_from_pep621(name: str = "", source: str = "pyproject.toml", output: str = "__about__.py") -> 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.
    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 output != "__about__.py" and "/" in output or "\\" in output:
            dir_path = "./"
        else:
            dir_path = f"./{project_name}"

        about_content, names = general.any_metadict(project_data)
        about_content = general.merge_sections(names, project_name, about_content)
        return write_to_file(dir_path, about_content, output)
    return "No [project] section found in pyproject.toml."
def generate_from_poetry(name: str = '', source: str = 'pyproject.toml', output: str = '__about__.py') ‑> 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.

Returns

str
Path to the file that was written.
Expand source code
def generate_from_poetry(name: str = "", source: str = "pyproject.toml", output: str = "__about__.py") -> 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.
    Returns:
        str: Path to the file that was written.
    """
    poetry_data = read_poetry_metadata(source)
    if poetry_data:
        project_name = poetry_data.get("name")
        if output != "__about__.py" and "/" in output or "\\" in output:
            dir_path = "./"
        else:
            dir_path = f"./{project_name}"
        about_content, names = general.any_metadict(poetry_data)
        about_content = general.merge_sections(names, project_name or "", about_content)
        # Define the content to write to the __about__.py file
        return filesystem.write_to_file(dir_path, about_content, output)
    return "No [tool.poetry] section found in pyproject.toml."
def generate_from_setup_cfg(name: str = '', source: str = 'setup.cfg', output: str = '__about__.py') ‑> 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.

Returns

str
Path to the file that was written.
Expand source code
def generate_from_setup_cfg(name: str = "", source: str = "setup.cfg", output: str = "__about__.py") -> 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.
    Returns:
        str: Path to the file that was written.
    """
    metadata = read_setup_cfg_metadata()
    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
        about_content, names = general.any_metadict(metadata)
        about_content = general.merge_sections(names, project_name, about_content)
        return write_to_file(dir_path, about_content, output)
    return "No [metadata] section found in setup.cfg."