Content and attachments

Inline content is stored in the XML document. Attachments are external files or directories referenced by src. Bundles with attachments can be ZIP archives or directories containing XML and files.

The <repository> and <file> elements can reference attachments. Their src paths are relative to the bundle’s XML file, including any containing basesrc prefix. A file without inline content defaults to src=name when src is omitted. Directory attachments include their nested files and directories. ZIP attachments are read directly from the archive.

The Repository and File classes expose attachment access. open() returns a binary stream that the caller closes. read() returns all file bytes. copy_to() copies content to a directory and refuses to overwrite an existing destination. A directory attachment requires a relative member path when opening or reading one of its files.

A file’s value property accesses inline Python values. It raises AttachmentContent for an external attachment. open() and read() work for both attachments and inline text or bytes. Inline strings are encoded as UTF-8 by these methods.

The content parameter of create() and recreate() accepts bytes for binary inline content. Export encodes these bytes in the XML document, and loading the bundle restores the original bytes.

New attachment paths must exist when the element is created. Source files must remain available until export. For directory export, source paths must not overlap the destination directory.

Recreating ordinary attachment elements keeps references to the original source without reading or copying its contents. The source files or ZIP bundle must remain available until the target bundle is exported. Self-test fixtures and report records have separate ownership rules, described in Self-test declarations and reports.

ZIP export copies compressed attachment bytes directly when the source and target use the same compression method, even when compresslevel is set. Copied attachments retain their original compression level. Set always_recompress=True to decompress and recompress ZIP attachments using the requested compression settings. Its default is False. A different compression method also requires recompression. Newly compressed files and the bundle XML use the requested compresslevel.

Added in version 9.0.0: Copying compressed ZIP attachments, and the always_recompress option that turns that copying off.

Directory export copies attachments out of their source storage. With sign_files=True, export also reads file contents to calculate and verify hashes.

Executable directory members

Added in version 9.0.0.

New filesystem attachments strip executable bits by default. To retain executable files inside a directory attachment, pass preserve_executable=True when creating the file. This example requires an existing dist-newstyle directory:

from pathlib import Path
from momotor.bundles import ProductBundle
from momotor.bundles.elements.files import File

bundle = ProductBundle()
build_directory = Path("dist-newstyle").resolve()
artifacts = File(bundle).create(
    name='dist-newstyle', src=build_directory, preserve_executable=True,
)
bundle.create(files=[artifacts])
bundle.to_file("artifacts.zip")

The option retains each file’s existing executable bits. It does not make other files executable or copy ownership, setuid, or setgid bits. Directory permissions use the normal filesystem defaults.

ZIP export stores executable bits on each member. Import, copying, recreation, and subsequent exports honor those bits without another opt-in. Directory exports also retain the bits, so importing an exported directory has the same behavior. Bundles that already lost the bits cannot recover them.

API

class momotor.bundles.elements.content.ContentAttachmentElement(bundle)

A full content element supporting attachments.

Contents can be string, bytes, boolean, integer, float or decimal.Decimal values, or an attachment referenced by src.

Parameters:

bundle (Bundle) – The Bundle containing this element

file_size(path=None)

Return the content length.

Inline strings report their character count. Inline bytes and external files report their byte count.

Parameters:

path (str | PurePath | None) – Optional relative path inside a directory attachment.

Return type:

int | None

Returns:

Content length.

Raises:
has_attachment_content()

Returns True if the element has an existing attachment

Return type:

bool

has_writable_content()

Return whether the element has an attachment or inline text or bytes.

is_dir(path=None)

Return whether the attachment or a member is a directory.

Parameters:

path (str | PurePath | None) – Optional relative path inside a directory attachment.

Return type:

bool

Returns:

False for inline content, otherwise the attachment’s directory status.

open(path=None)

Open inline text, inline bytes, or an attachment as a binary stream.

Inline strings are encoded as UTF-8. The caller closes the returned stream.

Parameters:

path (str | PurePath | None) – Relative member path for a directory attachment. Omit for inline content or a single file.

Return type:

IO[bytes]

Returns:

Readable binary stream.

Raises:

FileNotFoundError – If a member path is supplied for inline content, or an attachment is missing.

read(path=None)

Read inline text, inline bytes, or an attachment as bytes.

Parameters:

path (str | PurePath | None) – Relative member path for a directory attachment. Omit for inline content or a single file.

Return type:

bytes

Returns:

Complete file content. Inline strings are encoded as UTF-8.

Raises:

FileNotFoundError – If a member path is supplied for inline content, or an attachment is missing.

property type_: str | None

The type attribute.

class momotor.bundles.elements.content.ContentBasicElement(bundle)

A basic content element. Contents can be string or boolean values.

Parameters:

bundle (Bundle) – The Bundle containing this element

class momotor.bundles.elements.content.ContentFullElement(bundle)

A full content element.

Contents can be string, bytes, boolean, integer, float or decimal.Decimal values.

Parameters:

bundle (Bundle) – The Bundle containing this element

property encoding: str | None

encoding attribute: read-only, the encoding is automatically determined from the value

property type_: str | None

Scalar type of the value, such as string, integer, decimal, or boolean.

momotor.bundles.elements.content.NO_CONTENT = <object object>

A sentinel value indicating there is no content.

class momotor.bundles.elements.content.ContentElementBase(bundle)

Base class for elements with content, either as a value attribute, child nodes or as an attachment. Handles name, type, value and encoding attributes and child node content. This unifies the handling of <option>, <property> and <file> nodes

The subclasses ContentBasicElement, ContentFullElement and ContentAttachmentElement exist mainly to create the proper documentation of the properties and methods.

value attribute, attachments and child nodes with content are mutually exclusive. type has a double role: For nodes with a ‘value’ attribute it indicates the type (string, integer or float), for nodes with child content or attachment, it is the content mime type. encoding is only used with child content, supported encodings are base64 and quopri

See Content and attachments for details on content handling and attachments.

property value: PT

value attribute: The content.

Setting value to None will generate a node with a <none/> tag. Set value to NO_CONTENT to create an empty content node.

When accessing, raises NoContent if no content was set instead of returning NO_CONTENT, raises AttachmentContent if the node refers to an attachment using src.

class momotor.bundles.mixins.attachments.SrcAttachmentMixin(*args, **kwargs)

Mixin providing external file attachment functionality.

Adds support for referencing external files via the src attribute. Handles both filesystem and ZIP bundle-based attachments with validation, copying, and export capabilities.

See Content and attachments for details on how attachments are handled differently for new vs. existing bundles.

copy_to(destination, *, name=None)

Copy an attachment this element refers to, to given destination directory.

If the attachment is a file, creates a new file in the given directory. If name is provided this will be the name of the new file, otherwise the name of the source file is used.

If the attachment is a directory, copies the contents of the source directory to the destination directory. If name is provided it is created as a new directory inside the destination directory.

Will not overwrite an existing file or directory.

Parameters:
Raises:
Return type:

Path

directory_hashes(hash_names)

Calculate versioned digests of every regular descendant and relative name.

Parameters:

hash_names (Iterable[str]) – Algorithms used for the framed tree digest.

Return type:

dict[str, str]

Returns:

Hexadecimal directory-v1 digests; empty directories do not participate.

Raises:

NotADirectoryError – If this attachment is a file.

file_ctime(path=None)

Get file creation time for the attachment.

Return type:

struct_time | None

file_hashes(hash_names)

Calculate the hashes of the file.

Only for file attachments. Will return an empty dictionary if the file does not exist or is a directory.

Parameters:

hash_names (Iterable[str]) – Names of the hashes to calculate. Should be valid arguments to hashlib.new()

Return type:

dict[str, str]

Returns:

A dictionary hash-algorithm -> hash

file_size(path=None)

Get file size for the attachment.

Return type:

int | None

has_attachment_content()

Check if the element has attachment content.

Return type:

bool

Returns:

True if the element has an attachment with content

has_bundle()

Check if src references a file associated with a bundle.

Return type:

bool

Returns:

True if src references a file associated with a bundle

has_writable_content()

Check if the attachment content can be written.

Return type:

bool

Returns:

True if the element has attachment content that can be written

has_zip_bundle()

Check if src references a file in a zipped bundle.

Return type:

bool

Returns:

True if src references a file associated with a zipped bundle

is_dir(path=None)

Check if the attachment is a directory.

Parameters:

path (str | PurePath | None) – Optional subpath within the attachment

Return type:

bool

Returns:

True if the attachment (or subpath) refers to a directory

Raises:

FileNotFoundError – If the attachment or path doesn’t exist

iterdir(*, include_empty_root=False)

Recursively iterate the contents of a directory attachment. The returned paths are relative to self.absolute_path

If the attachment is an empty directory and include_empty_root is True, a single None value is yielded

Return type:

Generator[PurePath | None, None, None]

open(path=None)

Open the attachment file for reading. Handles opening files directly from filesystem and from zipped bundles

Parameters:

path (str | PurePath | None) – for directory attachments, path selects a file in that directory

Return type:

IO[bytes]

Returns:

the opened file

Raises:
  • FileNotFoundError – if the element has no attachment (when src is None), or when it’s a directory and path does not exist in that directory

  • IsADirectoryError – when it’s a directory

read(path=None)

Read the contents of the attachment

Parameters:

path (str | PurePath | None) – for directory attachments, path selects a file in that directory

Return type:

bytes

Returns:

A bytes object with the full file contents

validate_hashes(expected_hashes, *, kind=None)

Validate file hashes or an explicitly versioned directory digest.

Parameters:
  • expected_hashes (dict[str, str]) – Expected hexadecimal digests by algorithm.

  • kind (str | None) – directory-v1 for a directory, or None for ordinary file hashes.

Return type:

bool

Returns:

Whether all hashes match. Unknown kinds and file/directory substitutions fail.

property absolute_path: Path

Get the absolute path of the attachment referenced by src

Raises:

ZippedAttachment – when the attachment is in a zipped bundle and therefor has no filesystem path

bundle
property export_src: PurePath

The export path of the attachment, converted to be safe for use in ZIP files

property src: PurePath | None

src attribute: file path of the content.

property src_bundle: Bundle | None

If src is a relative path, the Bundle src references to.

An src attribute with a relative path is always associated with a bundle, either the current bundle or a source bundle retained by recreate(). The source bundle provides the base path for the relative path.

class momotor.bundles.mixins.attachments.AttachmentSrc(path=None, bundle=None, *, validate=True, preserve_executable=False)

Represents a source reference for bundle attachments.

Stores an absolute filesystem path or a path relative to a bundle. A missing path raises FileNotFoundError by default. With validate=False, a missing path emits a warning and becomes None.

Parameters:
  • path (PurePath | None) – Path to the attachment (absolute or relative to bundle)

  • bundle (Bundle | None) – Bundle containing the attachment (for relative paths)

  • validate – Whether to validate path existence during initialization

  • preserve_executable (bool) – Whether to retain source executable bits. Imported attachments retain them automatically.

Raises:

FileNotFoundError – If validate=True and the path doesn’t exist

bundle: Bundle | None = None
path: PurePath | None = None
preserve_executable: bool = False

Content type parameter

PT denotes the decoded Python value type in inherited content properties.

momotor.bundles.elements.content.PT = ~PT

Type variable.

Usage:

T = TypeVar('T')  # Can be anything
A = TypeVar('A', str, bytes)  # Must be str or bytes

Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function definitions. See class Generic for more information on generic types. Generic functions work as follows:

def repeat(x: T, n: int) -> List[T]:

‘’’Return a list containing n references to x.’’’ return [x]*n

def longest(x: A, y: A) -> A:

‘’’Return the longest of two strings.’’’ return x if len(x) >= len(y) else y

The latter example’s signature is essentially the overloading of (str, str) -> str and (bytes, bytes) -> bytes. Also note that if the arguments are instances of some subclass of str, the return type is still plain str.

At runtime, isinstance(x, T) and issubclass(C, T) will raise TypeError.

Type variables defined with covariant=True or contravariant=True can be used to declare covariant or contravariant generic types. See PEP 484 for more details. By default generic types are invariant in all type variables.

Type variables can be introspected. e.g.:

T.__name__ == ‘T’ T.__constraints__ == () T.__covariant__ == False T.__contravariant__ = False A.__constraints__ == (str, bytes)

Note that only type variables defined in global scope can be pickled.

NoContentType is the type of the NO_CONTENT sentinel. It distinguishes an omitted value from an explicit null value, represented by None.

momotor.bundles.elements.content.NoContentType = momotor.bundles.elements.content.NoContentType

NewType creates simple unique types with almost zero runtime overhead.

NewType(name, tp) is considered a subtype of tp by static type checkers. At runtime, NewType(name, tp) returns a dummy callable that simply returns its argument.

Usage:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

num = UserId(5) + 1     # type: int