Skip to content

Config Classes

David Reed edited this page Nov 3, 2022 · 11 revisions

Config Classes in CumulusCI Core

The config-class hierarchy looks like this:

classDiagram
    BaseConfig <|-- BaseTaskFlowConfig
    BaseConfig <|-- OrgConfig
    OrgConfig <|-- SfdxOrgConfig
    SfdxOrgConfig <|-- ScratchOrgConfig
    BaseTaskFlowConfig <|-- BaseProjectConfig
    BaseTaskFlowConfig <|-- UniversalConfig
    BaseConfig <|-- ProjectConfigPropertiesMixin
    ProjectConfigPropertiesMixin <|-- UniversalConfig
    ProjectConfigPropertiesMixin <|-- BaseProjectConfig
    BaseConfig <|-- ConnectedAppOAuthConfig
    FlowConfig <|-- BaseConfig
    TaskConfig <|-- BaseConfig
    BaseProjectKeychain <|-- EncryptedFileProjectKeychain
    ServiceConfig <|-- BaseConfig
    OAuth2ServiceConfig <|-- MarketingCloudServiceConfig
    ServiceConfig <|-- OAuth2ServiceConfig
    BaseConfig <|-- BaseProjectKeychain

Every config class has a config member. It's a dict, which is parsed from the YAML, and is therefore very difficult to type comprehensively.

Access to the config member is asymmetric. We have a __getattr__() override on BaseConfig that attempts to parse __-separated config paths and look them up dynamically in the config member. There's a new lookup() method that is the preferred route to dynamically lookup config paths (rather than using getattr()). However, updates to config are done by directly accessing the config member - BaseConfig does not override __setitem__() or similar. This is done either via assignment:

some_config.config["some_var"] = some_value

or dict methods:

some_config.config.update(some_dict)

This pattern breaks encapsulation and prevents us from using typing effectively on most config classes, and is a long-term challenge we need to fix.


Because of the way we've moved (some) config classes between modules in the past, while attempting to preserve backwards compatibility, the config classes are very sensitive to import order. Adding type annotations in ways that cause config modules to import from other config modules can cause very counterintuitive errors. Using if TYPE_CHECKING: as a guard around imports can help resolve this.

Small Challenges We Can Address

  • There's a potential issue in BaseConfig: the init method accepts a keychain optional kwarg but does nothing with it.
  • In ProjectConfig, replacing all of the repo_info machinery with a Pydantic settings class would be nifty.

Long-Term Challenges

  • Replacing untyped dicts with strictly-typed Pydantic models, and adjusting value access throughout the codebase.
  • Discarding backwards-compatible shims to address import-order dependencies.

Org Config

classDiagram
    BaseConfig <|-- OrgConfig
    OrgConfig <|-- SfdxOrgConfig
    SfdxOrgConfig <|-- ScratchOrgConfig

Token Refresh

The method refresh_oauth_token() takes a keychain and a connected app. We're not quite sure why, since the org config should know what to use. These parameters may have the sense of an override, and may be used in the web app context. We need to do some more review to be sure.

OAuth2Client.refresh_token() returns raw JSON. Could we be using a Pydantic model to parse this return and provide type safety?

Lazy-Loaded Properties

The OrgConfig class has many properties that lazy-load and cache from the org, including:

Package Version Caching

The OrgConfig class tracks the packages installed in the org using a lazy-loaded property, installed_packages. The type of this property is DefaultDict[str, List[VersionInfo]]. The keys are strings, which are either the package namespace or a string formatted as "namespace@1.2.3.4", with the package version.

This data is used to answer the questions:

  • Is Package A installed?
  • Is at least version N.M of Package A installed?

has_minimum_package_version() is the easiest entry point to this functionality. It takes a package identifier, which can be either a namespace or an 033 AllPackageId, and a version (str). It is then responsible for interprets the data structure and returning a result.

The data structure is also publicly exposed as installed_packages, and preflight checks often use it as

when: "'some_package' not in org_config.installed_packages"

Tasks that install packages are responsible for resetting the cache by calling reset_installed_packages().

Small Challenges We Can Address

  • refresh_oauth_token() takes a keychain parameter, but the OrgConfig has a keychain instance variable.