Extending the astrolabe

Attaching custom analysis methods to the Astrolabe class with plugins.

Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. x-iztro lets you attach your own rules as methods on the astrolabe class — the call syntax matches the built-in methods, and every astrolabe instance gets them.

The recipe

A plugin is a function that takes the Astrolabe class and attaches methods to it.

Write a function taking a type[Astrolabe]
Define the methods inside it and assign them onto the class
Load it with load_plugin
from x_iztro import Astro, Astrolabe, PalaceName
from x_iztro.plugin import load_plugin


def my_analysis(cls: type[Astrolabe]) -> None:
    """Adds two custom analysis methods to the astrolabe."""

    def major_star(self) -> str:
        """Major stars of the Soul palace (borrowing the opposite palace when empty), comma separated"""
        soul = self.palace(PalaceName.SOUL)
        source = soul.opposite_palace() if soul.is_empty() else soul
        return ",".join(s.name for s in source.major_stars)

    def five_elements_value(self) -> int:
        """The number of the five elements class"""
        return int(self.five_elements_class_key[-3])

    cls.major_star = major_star
    cls.five_elements_value = five_elements_value


load_plugin(my_analysis)

Usage

chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")

print(chart.major_star())

# the extension method follows the charting language
zh = Astro().by_solar("2000-8-16", 2, "female")
print(zh.major_star())

Output

emperor
紫微

load_plugin / load_plugins

Signature

def load_plugin(plugin: Plugin) -> None
def load_plugins(plugins: Iterable[Plugin]) -> None

Plugin is typed as Callable[[type[Astrolabe]], None].

Parameters

ParameterTypeRequiredDefaultDescription
pluginPluginYesA function taking the Astrolabe class
pluginsIterable[Plugin]YesSeveral plugins, loaded in order

Return value None. The methods land directly on the class.

Edge cases and pitfalls


Why this can be attached at all

Astrolabe is a frozen dataclass with slots=True, so nothing can be attached to an instance:

try:
    chart.foo = 1
except Exception as e:
    print(type(e).__name__, e)

Output

FrozenInstanceError cannot assign to field 'foo'

Attaching methods to the class is unaffected, and that is exactly the granularity a plugin wants: a plugin says "every astrolabe has this method", not "this one chart has an extra field".


Extending other types

The same recipe works for palaces and stars — just attach the methods to the corresponding class:

from x_iztro.models import Palace


def palace_analysis(cls: type[Palace]) -> None:
    def is_afflicted(self) -> bool:
        """Whether this palace is "afflicted": holds a malefic and carries ji"""
        sha = ["qingyangMin", "tuoluoMin", "huoxingMin",
               "lingxingMin", "dikongMin", "dijieMin"]
        return self.has_one_of(sha) and self.has_mutagen("sihuaJi")

    cls.is_afflicted = is_afflicted


palace_analysis(Palace)
for p in chart.palaces:
    if p.is_afflicted():
        print(p.name, "is afflicted")

Output

health is afflicted

load_plugin only accepts plugins that act on Astrolabe. To attach to another class, call the function directly as above — there is no magic in the plugin mechanism itself.


How to organize this

On this page