Skip to content

Base classes for a project.

Contents

Library: stores Genre classes in the hierarchy of their subclasses. library: the Library that Genre subclasses are added to. Genre: base class for every class that is stored in the library. Idea: settings of a project, with properties for building its workflow. Vertex: base class for nodes in a project workflow. Criteria: scores the results of a workflow. Report: base class for reports that describe a project.

Criteria(contents=None, parameters=dict()) dataclass

Bases: Genre, ABC

Scores the results of a workflow.

Criteria can be used directly, by wrapping a tool, or subclassed to override score. A Contest keeps the result with the highest score, and a Benchmark stops when test passes.

Parameters:

Name Type Description Default
contents str | Callable[..., Any] | None

tool that scores a result: a callable or the import path of one. Defaults to None.

None
parameters GenericDict

keyword arguments passed to the tool. Defaults to an empty dict.

dict()

parameters = dataclasses.field(default_factory=dict) class-attribute instance-attribute

Public Methods

score(item)

Returns the score of item. Higher scores are better.

Parameters:

Name Type Description Default
item Any

data or object to score.

required

Raises:

Type Description
NotImplementedError

if there is no tool in contents and a subclass does not override this method.

Returns:

Type Description
Any

The result of calling the tool with item and parameters.

Source code in src/chrisjen/base.py
def score(self, item: Any) -> Any:
    """Returns the score of `item`. Higher scores are better.

    Args:
        item: data or object to score.

    Raises:
        NotImplementedError: if there is no tool in `contents` and a
            subclass does not override this method.

    Returns:
        The result of calling the tool with `item` and `parameters`.

    """
    tool = self.contents
    if isinstance(tool, str):
        tool = utilities.import_object(tool)
    if tool is None:
        message = 'criteria has no tool: pass one as contents'
        raise NotImplementedError(message)
    return tool(item, **self.parameters)

test(item)

Returns whether item meets the criteria (has a true score).

Parameters:

Name Type Description Default
item Any

data or object to test.

required

Returns:

Type Description
bool

Whether the score of item is true.

Source code in src/chrisjen/base.py
def test(self, item: Any) -> bool:
    """Returns whether `item` meets the criteria (has a true score).

    Args:
        item: data or object to test.

    Returns:
        Whether the score of `item` is true.

    """
    return bool(self.score(item))

Genre() dataclass

Bases: ABC

Stores itself in the project library.

Every subclass of Genre is added to library when it is defined. A subclass that lists abc.ABC among its bases is abstract: it forms a new layer of Library.contents. Any other subclass is stored in the layer of its nearest abstract ancestor.

Idea() dataclass

Bases: Settings, Genre

Loads and stores configuration settings.

Idea is a subclass of the Settings class in the bobbie repo. See its documentation for information about its functionality. https://withprecedent.github.io/bobbie/

Idea adds properties (workers, steps, techniques, designs, criteria, and parameters) and a get_settings method that read the parts of a chrisjen workflow from the settings. Its fields (contents, default_factory, and name) are those of bobbie.Settings.

criteria property

Returns the criteria of the workers and steps in the settings.

In each worker section, a "criterion" setting is the criterion of the worker and a "{name}_criterion" setting is the criterion of name.

Returns:

Type Description
dict[str, str]

A dict mapping names (the prefix of the setting, or the worker name for "criterion") to criteria.

designs property

Returns the designs of the workers in the settings.

In each worker section, a "design" setting is the design of the worker and a "{name}_design" setting is the design of name. A worker without a design uses options._DEFAULT_DESIGN.

Returns:

Type Description
dict[str, str]

A dict mapping names (the prefix of the setting, or the worker name for "design") to designs.

parameters property

Returns the parameters sections of the settings.

Returns:

Type Description
dict[str, dict[str, Any]]

A dict mapping the prefix of each section that ends in "_parameters" to that section.

steps property

Returns the steps of the workers in the settings.

In each worker section, a "steps" setting lists the steps of the worker and a "{name}_steps" setting lists the steps of name. A "workers" or "{name}_workers" setting (as in the project section) lists steps the same way.

Returns:

Type Description
dict[str, list[str]]

A dict mapping names (the prefix of the setting, or the worker name for "steps") to lists of step names.

techniques property

Returns the techniques of the workers and steps in the settings.

In each worker section, a "techniques" setting lists the techniques of the worker and a "{name}_techniques" setting lists the techniques of name (usually a step).

Returns:

Type Description
dict[str, list[str]]

A dict mapping names (the prefix of the setting, or the worker name for "techniques") to lists of technique names.

workers property

Returns the worker sections of the settings.

The project section (whose name ends in "_project") is included under the name of the project, the part before "_project", so its settings are found by the other properties as well.

Returns:

Type Description
dict[str, dict[str, Any]]

A dict mapping the name of each section of contents that does not end in "parameters" and is not in options._SPECIAL_SETTINGS to that section.

get_settings(name, keys)

Returns the settings in keys that belong to name.

A setting belongs to name if it is in the section for name or if it is named "{name}_{key}" in any worker section. This is how settings such as "max_iterations" reach the class that name is built from.

Parameters:

Name Type Description Default
name str

name of a worker, step, or technique.

required
keys Sequence[str]

names of the settings to look for.

required

Returns:

Type Description
dict[str, Any]

A dict mapping each key in keys that is found to its value.

Source code in src/chrisjen/base.py
def get_settings(self, name: str, keys: Sequence[str]) -> dict[str, Any]:
    """Returns the settings in `keys` that belong to `name`.

    A setting belongs to `name` if it is in the section for `name` or if
    it is named "{name}_{key}" in any worker section. This is how settings
    such as "max_iterations" reach the class that `name` is built from.

    Args:
        name: name of a worker, step, or technique.
        keys: names of the settings to look for.

    Returns:
        A `dict` mapping each key in `keys` that is found to its value.

    """
    found = {}
    for key in keys:
        settings = self._find_settings(key)
        if name in settings:
            found[key] = settings[name]
    return found

Library(contents=dict()) dataclass

Bases: MutableMapping[str, Any]

Stores Genre classes in the hierarchy of their subclasses.

contents is a nested dict that follows the subclasses of Genre. An abstract class (one that lists abc.ABC among its bases) is only a key: its value is a new layer for its subclasses, and the class itself is not stored. Any other class is stored, under its name, in the layer of its nearest abstract ancestor (or at the top level, if it has none). For example, Vertex, Step, and Worker are abstract, so if Scaler is a subclass of Technique and Scale is a subclass of Step:

{
    'vertex': {
        'technique': Technique,
        'scaler': Scaler,
        'step': {'scale': Scale},
        'worker': {'flow': Flow, 'contest': Contest},
    }
}

A class can also be added under another name (NullVertex is also stored as "none"). all is a flat dict of every stored class, and borrow finds classes by name.

Attributes:

Name Type Description
contents Layer

nested dictionary of Genre subclasses. Keys are the names of the subclasses (by default, snake case versions of their class names). Values are either subclasses or nested dictionaries containing subclasses.

all property

Returns a flattened dictionary of all classes in the library.

contents = dataclasses.field(default_factory=dict) class-attribute instance-attribute

Properties

genres property

Returns names of the genres (abstract classes) at every level.

plurals property

Returns a list of pluralized names of genres.

add(item, name=None)

Adds item to its place in contents.

An abstract class (one that lists abc.ABC among its bases) adds a new, empty layer named for it, unless that layer already exists. Any other class is stored under its name in the layer of its nearest abstract ancestor. A direct subclass of Genre is at the top level.

Parameters:

Name Type Description Default
item type

Genre subclass to add.

required
name str | None

name to store item under. Defaults to None, which uses the snake case name of the class.

None

Raises:

Type Description
TypeError

if item is not a subclass of Genre.

ValueError

if item is Genre itself, or if its name is already used in the same layer for a different kind of entry (a class where a layer is, or a layer where a class is).

Source code in src/chrisjen/base.py
def add(self, item: type, name: str | None = None) -> None:
    """Adds `item` to its place in `contents`.

    An abstract class (one that lists `abc.ABC` among its bases) adds a
    new, empty layer named for it, unless that layer already exists. Any
    other class is stored under its name in the layer of its nearest
    abstract ancestor. A direct subclass of `Genre` is at the top level.

    Args:
        item: `Genre` subclass to add.
        name: name to store `item` under. Defaults to `None`, which uses
            the snake case name of the class.

    Raises:
        TypeError: if `item` is not a subclass of `Genre`.
        ValueError: if `item` is `Genre` itself, or if its name is already
            used in the same layer for a different kind of entry (a class
            where a layer is, or a layer where a class is).

    """
    if not (inspect.isclass(item) and issubclass(item, Genre)):
        message = f'item must be a subclass of Genre, not {item!r}'
        raise TypeError(message)
    if item is Genre:
        message = 'Genre itself cannot be added: add its subclasses'
        raise ValueError(message)
    name = self._get_name(item, name)
    layer = self._get_layer(
        utilities._get_path(item, Genre, self._get_name))
    existing = layer.get(name)
    if utilities._is_abstract(item):
        if existing is not None and not isinstance(existing, dict):
            message = (
                f'{name!r} is already a stored class, so the abstract '
                f'class {item.__name__} cannot form a genre with it'
            )
            raise ValueError(message)
        # Keeps the layer (and what is in it) if it already exists.
        layer.setdefault(name, {})
    else:
        if isinstance(existing, dict):
            message = (
                f'{name!r} is already a genre, so the class '
                f'{item.__name__} cannot be stored with that name'
            )
            raise ValueError(message)
        layer[name] = item

borrow(names, genre=None)

Returns the first class found that is named in names.

Listing more than one name allows a specific class to be used if it exists, with a general one as a fallback. For example, a worker named "analyst" can use an Analyst class, if there is one, and otherwise the class of its design.

Parameters:

Name Type Description Default
names str | Sequence[str]

name or names of classes, in the order to look for them.

required
genre str | type[Genre] | None

name of the genre (or the abstract class) to look in. Defaults to None, which looks in the whole library.

None

Raises:

Type Description
KeyError

if none of names are in the library (or genre), or if genre is not a genre in the library.

Returns:

Type Description
type[Genre]

The first class found.

Source code in src/chrisjen/base.py
def borrow(
    self,
    names: str | Sequence[str],
    genre: str | type[Genre] | None = None) -> type[Genre]:
    """Returns the first class found that is named in `names`.

    Listing more than one name allows a specific class to be used if it
    exists, with a general one as a fallback. For example, a worker named
    "analyst" can use an `Analyst` class, if there is one, and otherwise
    the class of its design.

    Args:
        names: name or names of classes, in the order to look for them.
        genre: name of the genre (or the abstract class) to look in.
            Defaults to `None`, which looks in the whole library.

    Raises:
        KeyError: if none of `names` are in the library (or `genre`), or
            if `genre` is not a genre in the library.

    Returns:
        The first class found.

    """
    if genre is None:
        classes = self.all
    else:
        classes = utilities._flatten_dict(self.get_genre(genre))
    for name in utilities._iterify(names):
        if name in classes:
            return classes[name]
    message = f'no class named {names!r} is in the library'
    raise KeyError(message)

classify(item)

Returns the genre (abstract base class) that item is in.

Parameters:

Name Type Description Default
item str | type[Genre] | Genre

Genre subclass, subclass instance, or its name.

required

Raises:

Type Description
ValueError

if item is not in the library, or if it is at the top level of the library (and so is not in a genre).

Returns:

Type Description
str

The name of the genre to which item belongs: the innermost layer that it is in.

Source code in src/chrisjen/base.py
def classify(self, item: str | type[Genre] | Genre) -> str:
    """Returns the genre (abstract base class) that `item` is in.

    Args:
        item: `Genre` subclass, subclass instance, or its name.

    Raises:
        ValueError: if `item` is not in the library, or if it is at the
            top level of the library (and so is not in a genre).

    Returns:
        The name of the genre to which `item` belongs: the innermost layer
            that it is in.

    """
    if isinstance(item, str):
        genre = utilities._find_genre(self.contents, item)
        if genre is utilities._MISSING:
            message = f'{item!r} is not in the library'
            raise ValueError(message)
    else:
        if not inspect.isclass(item):
            item = item.__class__
        if not issubclass(item, Genre) or item is Genre:
            message = f'{item!r} is not in the library'
            raise ValueError(message)
        path = utilities._get_path(item, Genre, self._get_name)
        genre = path[-1] if path else None
    if genre is None:
        message = f'{item!r} is at the top level, so it is not in a genre'
        raise ValueError(message)
    return genre

delete(item)

Deletes the class or layer named item, wherever it is.

Deleting a layer deletes everything in it.

Parameters:

Name Type Description Default
item str

name of a class or a layer.

required

Raises:

Type Description
KeyError

if item is not in contents.

Source code in src/chrisjen/base.py
def delete(self, item: str) -> None:
    """Deletes the class or layer named `item`, wherever it is.

    Deleting a layer deletes everything in it.

    Args:
        item: name of a class or a layer.

    Raises:
        KeyError: if `item` is not in `contents`.

    """
    removed: set[str] = set()
    def remove(layer: Layer) -> None:
        for key in list(layer):
            value = layer[key]
            if key == item:
                removed.add(key)
                if isinstance(value, dict):
                    removed.update(utilities._get_all_keys(value))
                del layer[key]
            elif isinstance(value, dict):
                remove(value)
    remove(self.contents)
    if not removed:
        message = f'{item!r} is not in the library'
        raise KeyError(message)

get_genre(item)

Returns the layer of contents formed by a genre abstract class.

Parameters:

Name Type Description Default
item str | type[Genre]

abstract Genre subclass or its name.

required

Raises:

Type Description
KeyError

if there is no layer for item.

Returns:

Type Description
Layer

The nested dict of the subclasses of item.

Source code in src/chrisjen/base.py
def get_genre(
    self,
    item: str | type[Genre]) -> Layer:
    """Returns the layer of `contents` formed by a genre abstract class.

    Args:
        item: abstract `Genre` subclass or its name.

    Raises:
        KeyError: if there is no layer for `item`.

    Returns:
        The nested `dict` of the subclasses of `item`.

    """
    name = item if isinstance(item, str) else self._get_name(item)
    found = utilities._find(self.contents, name)
    if not isinstance(found, dict):
        message = f'{name!r} is not a layer of the library'
        raise KeyError(message)
    return found

Report(contents=None) dataclass

Bases: Genre, ABC

Describes a project after it is applied.

Project.apply calls generate after each run of the workflow.

Parameters:

Name Type Description Default
contents Any

data for the report. Defaults to None.

None

contents = dataclasses.field(default=None) class-attribute instance-attribute

Required Methods

generate(project) abstractmethod

Generates the report for project.

Parameters:

Name Type Description Default
project Project

the project to report on, after it has been applied.

required

Returns:

Type Description
Any

The generated report.

Source code in src/chrisjen/base.py
@abc.abstractmethod
def generate(self, project: interface.Project) -> Any:
    """Generates the report for `project`.

    Args:
        project: the project to report on, after it has been applied.

    Returns:
        The generated report.

    """
    raise NotImplementedError

Vertex(contents=None, name=None, parameters=dict()) dataclass

Bases: Labeled, Node, Genre, ABC

Base class for nodes in a chrisjen workflow.

Vertexes are hashed and compared by name, so a str equal to the name of a node can be used in its place. Each vertex is applied to an item with apply and can be built from a project's settings with build. The subclasses are in the nodes and workers modules.

Parameters:

Name Type Description Default
contents Any | None

item(s) to be applied to the item passed to apply. Defaults to None.

None
name str | None

name used to refer to the node in a workflow. Defaults to None, in which case the name is inferred from the class or contents.

None
parameters GenericDict

keyword arguments passed to implement by apply. Defaults to an empty dict.

dict()

parameters = dataclasses.field(default_factory=dict) class-attribute instance-attribute

Class Methods

apply(item, **kwargs)

Applies this node to item.

Parameters:

Name Type Description Default
item Any

data or object (often the result of the previous node) to which this node should be applied.

required
**kwargs Any

keyword arguments for implement that take precedence over parameters.

{}

Returns:

Type Description
Any

The result of applying this node to item.

Source code in src/chrisjen/base.py
def apply(self, item: Any, **kwargs: Any) -> Any:
    """Applies this node to `item`.

    Args:
        item: data or object (often the result of the previous node) to
            which this node should be applied.
        **kwargs: keyword arguments for `implement` that take precedence
            over `parameters`.

    Returns:
        The result of applying this node to `item`.

    """
    return self.implement(item, **{**self.parameters, **kwargs})

build(name, project, parameters=None, **kwargs) classmethod

Builds an instance named name from the settings of project.

Settings that belong to name (see Idea.get_settings) and match a field of the class are passed to the constructor, so a setting such as "max_iterations" reaches the class that uses it. The "{name}_parameters" section of the settings is added to the default parameters of the class. Subclasses can override this method to build themselves differently.

Parameters:

Name Type Description Default
name str

name of the node.

required
project Project

the project the node belongs to.

required
parameters GenericDict | None

more parameters, which take precedence over those in the settings. Defaults to None.

None
**kwargs Any

other arguments for the constructor, which take precedence over the settings.

{}

Returns:

Type Description
Vertex

The built instance.

Source code in src/chrisjen/base.py
@classmethod
def build(
    cls,
    name: str,
    project: interface.Project,
    parameters: GenericDict | None = None,
    **kwargs: Any) -> Vertex:
    """Builds an instance named `name` from the settings of `project`.

    Settings that belong to `name` (see `Idea.get_settings`) and match a
    field of the class are passed to the constructor, so a setting such as
    "max_iterations" reaches the class that uses it. The "{name}_parameters"
    section of the settings is added to the default parameters of the
    class. Subclasses can override this method to build themselves
    differently.

    Args:
        name: name of the node.
        project: the project the node belongs to.
        parameters: more parameters, which take precedence over those in
            the settings. Defaults to `None`.
        **kwargs: other arguments for the constructor, which take
            precedence over the settings.

    Returns:
        The built instance.

    """
    fields = [
        field.name for field in dataclasses.fields(cls)
        if field.name not in _NOT_SETTINGS]
    settings = project.idea.get_settings(name, fields)
    vertex = cls(name = name, **{**settings, **kwargs})
    vertex.parameters.update(project.idea.parameters.get(name, {}))
    vertex.parameters.update(parameters or {})
    return vertex

implement(item, **kwargs) abstractmethod

Applies contents to item.

Parameters:

Name Type Description Default
item Any

data or object to which contents should be applied.

required
**kwargs Any

parameters for the implementation.

{}

Returns:

Type Description
Any

The result of applying contents to item.

Source code in src/chrisjen/base.py
@abc.abstractmethod
def implement(self, item: Any, **kwargs: Any) -> Any:
    """Applies `contents` to `item`.

    Args:
        item: data or object to which `contents` should be applied.
        **kwargs: parameters for the implementation.

    Returns:
        The result of applying `contents` to `item`.

    """