Skip to content

Environment schema#

Environment#

BaseCompilationConfig #

Bases: BaseModel

Parameters:

Name Type Description Default
commands List[str] | None

Commands to compile the program.

[]
sandbox EnvironmentSandbox | None

Sandbox configuration to use when compiling for this language.

None
passthrough bool | None

Whether to pass through the compilable as an executable file.

None
Source code in rbx/box/environment.py
class BaseCompilationConfig(BaseModel):
    model_config = ConfigDict(extra='forbid')

    commands: Optional[List[str]] = Field(
        default=[],
        description="""Commands to compile the program.""",
    )

    sandbox: Optional[EnvironmentSandbox] = Field(
        default=None,
        description="""Sandbox configuration to use when compiling for this language.""",
    )

    passthrough: Optional[bool] = Field(
        default=None,
        description="""Whether to pass through the compilable as an executable file.""",
    )

BaseExecutionConfig #

Bases: BaseModel

Parameters:

Name Type Description Default
command str | None

Command to run the program.

None
sandbox EnvironmentSandbox | None

Sandbox configuration to use when executing for this language.

None
problemLimits Limits

Original limits of the problem.

<dynamic>
Source code in rbx/box/environment.py
class BaseExecutionConfig(BaseModel):
    model_config = ConfigDict(extra='forbid')

    command: Optional[str] = Field(
        default=None,
        description="""Command to run the program.""",
    )

    sandbox: Optional[EnvironmentSandbox] = Field(
        default=None,
        description="""Sandbox configuration to use when executing for this language.""",
    )

    problemLimits: Limits = Field(
        default_factory=Limits,
        description="""Original limits of the problem.""",
    )

CompilationConfig #

Bases: BaseCompilationConfig

Parameters:

Name Type Description Default
solutionOverrides SolutionCompilationOverrides

Overrides to apply when compiling solutions for this language.

<dynamic>
Source code in rbx/box/environment.py
class CompilationConfig(BaseCompilationConfig):
    solutionOverrides: SolutionCompilationOverrides = Field(
        default_factory=SolutionCompilationOverrides,
        description="""Overrides to apply when compiling solutions for this language.""",
    )

Environment #

Bases: BaseModel

Parameters:

Name Type Description Default
defaultFileMapping FileMapping | None

Default mapping for files within the sandbox. Fields in the mapping can be individually overridden in the language configuration.

None
defaultCompilation CompilationConfig | None

Default compilation configuration to use when compiling programs. Fields in the compilation config can be individually overridden in the language configuration.

None
defaultExecution ExecutionConfig | None

Default execution configuration to use when running programs. Fields in the execution config can be individually overridden in the language configuration.

None
languages List[EnvironmentLanguage]

Configuration for each language supported in this environment.

[]
sandbox str

Identifier of the sandbox used by this environment (e.g. "stupid")

'stupid'
timing TimingConfig

Timing configuration for the environment.

<dynamic>
extensions Extensions | None

Extensions to be added to the environment.

None
buildDir Path

Directory to store the build files.

PosixPath('build')
Source code in rbx/box/environment.py
class Environment(BaseModel):
    model_config = ConfigDict(extra='forbid')

    defaultFileMapping: Optional[FileMapping] = Field(
        default=None,
        description="""Default mapping for files within the sandbox. Fields in the mapping can be
individually overridden in the language configuration.""",
    )

    defaultCompilation: Optional[CompilationConfig] = Field(
        default=None,
        description="""Default compilation configuration to use when compiling programs. Fields in
the compilation config can be individually overridden in the language configuration.""",
    )

    defaultExecution: Optional[ExecutionConfig] = Field(
        default=None,
        description="""Default execution configuration to use when running programs. Fields in the
execution config can be individually overridden in the language configuration.""",
    )

    languages: List[EnvironmentLanguage] = Field(
        default=[],
        description="""Configuration for each language supported in this environment.""",
    )

    sandbox: str = Field(
        default='stupid',
        description="""Identifier of the sandbox used by this environment (e.g. "stupid")""",
    )

    timing: TimingConfig = Field(
        default_factory=TimingConfig,
        description="""Timing configuration for the environment.""",
    )

    extensions: Optional[Extensions] = Field(
        default=None,
        description="""Extensions to be added to the environment.""",
    )

    buildDir: pathlib.Path = Field(
        default=pathlib.Path('build'),
        description="""Directory to store the build files.""",
    )

EnvironmentLanguage #

Bases: BaseModel

Parameters:

Name Type Description Default
name str

Identifier of this language within this environment.

required
readableName str | None

Readable name for this language.

None
extension str

File extension supported by this language. If there's only one language that supports a certain file extension in the environment, the tool will automatically identify the language based on such extension.

required
extraExtensions List[str]

Extra file extensions supported by this language. If not specified, the tool will automatically identify the language based on such extensions.

<dynamic>
compilation CompilationConfig | None

Compilation config to use when compiling programs for this language.

None
execution ExecutionConfig

Execution config to use when running programs for this language.

required
fileMapping FileMapping | None

Mapping for files within the sandbox. If not specified, the default mapping for the environment will be used.

None
extensions LanguageExtensions | None

Extensions to apply for this language.

None
linters List[LinterConfig]

Linters to run for this language during compilation.

<dynamic>
scanners List[str]

Dependency scanners (by registry name) to apply for this language, in addition to the ones automatically selected by language kind.

<dynamic>
timing LanguageTimingConfig | None

Per-language overrides for timing configuration (e.g. wall time).

None
Source code in rbx/box/environment.py
class EnvironmentLanguage(BaseModel):
    model_config = ConfigDict(extra='forbid')

    name: str = Field(
        description="""Identifier of this language within this environment."""
    )

    readableName: Optional[str] = Field(
        default=None,
        description="""Readable name for this language.""",
    )

    extension: str = Field(
        description="""File extension supported by this language. If there's only one language
that supports a certain file extension in the environment, the tool
will automatically identify the language based on such extension."""
    )

    extraExtensions: List[str] = Field(
        default_factory=list,
        description="""Extra file extensions supported by this language. If not specified, the tool
will automatically identify the language based on such extensions.""",
    )

    compilation: Optional[CompilationConfig] = Field(
        default=None,
        description="""Compilation config to use when compiling programs for this language.""",
    )

    execution: ExecutionConfig = Field(
        description="""Execution config to use when running programs for this language."""
    )

    fileMapping: Optional[FileMapping] = Field(
        default=None,
        description="""Mapping for files within the sandbox. If not specified, the default mapping
for the environment will be used.""",
    )

    extensions: Optional[LanguageExtensions] = Field(
        default=None,
        description="""Extensions to apply for this language.""",
    )

    linters: List[LinterConfig] = Field(
        default_factory=list,
        description="""Linters to run for this language during compilation.""",
    )

    scanners: List[str] = Field(
        default_factory=list,
        description="""Dependency scanners (by registry name) to apply for this
language, in addition to the ones automatically selected by language kind.""",
    )

    timing: Optional[LanguageTimingConfig] = Field(
        default=None,
        description="""Per-language overrides for timing configuration (e.g. wall time).""",
    )

    @field_validator('linters', mode='before')
    @classmethod
    def _coerce_linter_shorthand(cls, v):
        if v is None:
            return []
        return [{'name': item} if isinstance(item, str) else item for item in v]

    def get_extension(self, name: str, _: Type[T]) -> Optional[T]:
        if self.extensions is None:
            return None
        if not hasattr(self.extensions, name):
            return None
        return getattr(self.extensions, name)

    def get_extension_or_default(self, name: str, cls: Type[T]) -> T:
        return self.get_extension(name, cls) or cls()

EnvironmentSandbox #

Bases: BaseModel

Parameters:

Name Type Description Default
maxProcesses int | None

Max. number of process to allow to run concurrently for the program.

1
timeLimit int | None

Time limit in milliseconds to allow the program to run.

None
wallTimeLimit int | None

Wall time limit in milliseconds to allow the program to run.

None
memoryLimit int | None

Memory limit in MiB.

None
fileSizeLimit int | None

File size limit in KiB

None
stackLimit int | None

Stack limit in MiB.

None
preserveEnv bool | None

Whether to preserve env. variables coming from the host.

False
mirrorDirs List[str] | None

Directories in the host that should be read-only exposed to the sandbox.

[]
Source code in rbx/box/environment.py
class EnvironmentSandbox(BaseModel):
    model_config = ConfigDict(extra='forbid')

    maxProcesses: Optional[int] = Field(
        default=1,
        description="""Max. number of process to allow to run concurrently for the program.""",
    )

    timeLimit: Optional[int] = Field(
        default=None,
        description="""Time limit in milliseconds to allow the program to run.""",
    )

    wallTimeLimit: Optional[int] = Field(
        default=None,
        description="""Wall time limit in milliseconds to allow the program to run.""",
    )

    memoryLimit: Optional[int] = Field(
        default=None,
        description="""Memory limit in MiB.""",
    )

    fileSizeLimit: Optional[int] = Field(
        default=None,
        description="""File size limit in KiB""",
    )

    stackLimit: Optional[int] = Field(
        default=None,
        description="""Stack limit in MiB.""",
    )

    preserveEnv: Optional[bool] = Field(
        default=False,
        description="""Whether to preserve env. variables coming from the host.""",
    )

    mirrorDirs: Optional[List[str]] = Field(
        default=[],
        description="""Directories in the host that should be read-only exposed to the sandbox.""",
    )

ExecutionConfig #

Bases: BaseExecutionConfig

Parameters:

Name Type Description Default
solutionOverrides SolutionExecutionOverrides

Overrides to apply when executing solutions for this language.

<dynamic>
Source code in rbx/box/environment.py
class ExecutionConfig(BaseExecutionConfig):
    solutionOverrides: SolutionExecutionOverrides = Field(
        default_factory=SolutionExecutionOverrides,
        description="""Overrides to apply when executing solutions for this language.""",
    )

FileMapping #

Bases: BaseModel

Parameters:

Name Type Description Default
input str

Path where to copy the stdin file to before running the program, relative to the sandbox root.

'stdin'
output str

Path where to output the stdout file after running the program, relative to the sandbox root.

'stdout'
error str

Path where to output the stderr file after running the program, relative to the sandbox root.

'stderr'
capture str

Path where to output the capture file after running the program, relative to the sandbox root.

'capture'
compilable str

Path where to copy the compilable file to before compiling the program, relative to the sandbox root.

'{source}'
executable str

Path to where to output the executable file after compiling the program, relative to the sandbox root.

'executable'
Source code in rbx/box/environment.py
class FileMapping(BaseModel):
    model_config = ConfigDict(extra='forbid')

    input: str = Field(
        default='stdin',
        description="""Path where to copy the stdin file to before running the program,
relative to the sandbox root.""",
    )

    output: str = Field(
        default='stdout',
        description="""Path where to output the stdout file after running the program,
relative to the sandbox root.""",
    )

    error: str = Field(
        default='stderr',
        description="""Path where to output the stderr file after running the program,
relative to the sandbox root.""",
    )

    capture: str = Field(
        default='capture',
        description="""Path where to output the capture file after running the program,
relative to the sandbox root.""",
    )

    compilable: str = Field(
        default='{source}',
        description="""Path where to copy the compilable file to before compiling the program,
relative to the sandbox root.""",
    )

    executable: str = Field(
        default='executable',
        description="""Path to where to output the executable file after compiling the program,
relative to the sandbox root.""",
    )

LanguageGroup #

Bases: BaseModel

Parameters:

Name Type Description Default
languages List[str]

rbx language names that share a single estimated time limit.

required
whenEmpty LanguageGroupFallback | None

How to derive a TL for this group when it has no solutions.

None
Source code in rbx/box/environment.py
class LanguageGroup(BaseModel):
    model_config = ConfigDict(extra='forbid')

    languages: List[str] = Field(
        description="""rbx language names that share a single estimated time limit.""",
    )
    whenEmpty: Optional[LanguageGroupFallback] = Field(
        default=None,
        description="""How to derive a TL for this group when it has no solutions.""",
    )

LanguageGroupFallback #

Bases: BaseModel

Parameters:

Name Type Description Default
relativeTo str | None

A language name whose group's estimated TL this empty group is relative to. If omitted, the multiplier is applied to the base estimate.

None
multiplier float

Slope applied to the reference TL when this group has no solutions. The resolved TL is multiplier * reference + increment.

required
increment int | None

Constant offset (in milliseconds) added on top of the multiplied reference TL when this group has no solutions. The resolved TL is multiplier * reference + increment.

None
Source code in rbx/box/environment.py
class LanguageGroupFallback(BaseModel):
    model_config = ConfigDict(extra='forbid', frozen=True)

    relativeTo: Optional[str] = Field(
        default=None,
        description="""A language name whose group's estimated TL this empty group is
relative to. If omitted, the multiplier is applied to the base estimate.""",
    )
    multiplier: float = Field(
        gt=0,
        description="""Slope applied to the reference TL when this group has no
solutions. The resolved TL is ``multiplier * reference + increment``.""",
    )
    increment: Optional[int] = Field(
        default=None,
        description="""Constant offset (in milliseconds) added on top of the
multiplied reference TL when this group has no solutions. The resolved TL is
``multiplier * reference + increment``.""",
    )

LanguageTimingConfig #

Bases: BaseModel

Parameters:

Name Type Description Default
wallTimeMultiplier float | None

Overrides the environment wall-time multiplier a for this language.

None
wallTimeIncrement int | None

Overrides the environment wall-time increment b (in milliseconds) for this language.

None
Source code in rbx/box/environment.py
class LanguageTimingConfig(BaseModel):
    model_config = ConfigDict(extra='forbid')

    wallTimeMultiplier: Optional[float] = Field(
        default=None,
        ge=1.0,
        description="""Overrides the environment wall-time multiplier `a` for this language.""",
    )

    wallTimeIncrement: Optional[int] = Field(
        default=None,
        ge=0,
        description="""Overrides the environment wall-time increment `b` (in milliseconds) for this language.""",
    )

LinterConfig #

Bases: BaseModel

Parameters:

Name Type Description Default
name str

Name of the linter to run (see registry).

required
applies_to List[AssetKind] | None

Asset kinds this linter applies to. None means all kinds.

None
Source code in rbx/box/environment.py
class LinterConfig(BaseModel):
    model_config = ConfigDict(extra='forbid')

    name: str = Field(description='Name of the linter to run (see registry).')
    applies_to: Optional[List[AssetKind]] = Field(
        default=None,
        description='Asset kinds this linter applies to. None means all kinds.',
    )

    @field_validator('applies_to', mode='before')
    @classmethod
    def _normalize_applies_to(cls, v):
        if v is None:
            return None
        out = []
        for item in v:
            if isinstance(item, AssetKind):
                out.append(item)
                continue
            token = str(item).rstrip('s')  # 'generators' -> 'generator'
            out.append(AssetKind(token))
        return out

SolutionCompilationOverrides #

Bases: BaseCompilationConfig

Source code in rbx/box/environment.py
class SolutionCompilationOverrides(BaseCompilationConfig):
    pass

SolutionExecutionOverrides #

Bases: BaseExecutionConfig

Source code in rbx/box/environment.py
class SolutionExecutionOverrides(BaseExecutionConfig):
    pass

TimingConfig #

Bases: BaseModel

Parameters:

Name Type Description Default
formula str

Formula to use to calculate the time limit for the environment.

'step_up(max(fastest * 3, slowest * 1.5), 100)'
groups List[LanguageGroup]

Groups of related languages that share an estimated time limit.

<dynamic>
wallTimeMultiplier float

Default multiplier a in the wall-time formula a*x + b, where x is the expanded CPU time limit.

2.0
wallTimeIncrement int

Default increment b (in milliseconds) in the wall-time formula a*x + b.

0
Source code in rbx/box/environment.py
class TimingConfig(BaseModel):
    model_config = ConfigDict(extra='forbid')

    formula: str = Field(
        default='step_up(max(fastest * 3, slowest * 1.5), 100)',
        description="""Formula to use to calculate the time limit for the environment.""",
    )

    groups: List[LanguageGroup] = Field(
        default_factory=list,
        description="""Groups of related languages that share an estimated time limit.""",
    )

    wallTimeMultiplier: float = Field(
        default=2.0,
        ge=1.0,
        description="""Default multiplier `a` in the wall-time formula `a*x + b`, where `x` is the expanded CPU time limit.""",
    )

    wallTimeIncrement: int = Field(
        default=0,
        ge=0,
        description="""Default increment `b` (in milliseconds) in the wall-time formula `a*x + b`.""",
    )

    @model_validator(mode='after')
    def _validate_disjoint_groups(self):
        seen: set[str] = set()
        for group in self.groups:
            for lang in group.languages:
                if lang in seen:
                    raise ValueError(
                        f'Language {lang!r} appears in more than one timing group; '
                        'groups must be disjoint.'
                    )
                seen.add(lang)
        return self

apply_walltime_formula(cpu_tl_ms, coeffs) #

Applies wall = a*x + b, where x is the expanded CPU time limit (ms).

Source code in rbx/box/environment.py
def apply_walltime_formula(cpu_tl_ms: int, coeffs: Tuple[float, int]) -> int:
    """Applies wall = a*x + b, where x is the expanded CPU time limit (ms)."""
    multiplier, increment = coeffs
    return int(cpu_tl_ms * multiplier + increment)

compute_walltime(cpu_tl_ms, language) #

Computes the wall-time limit (ms) for a CPU time limit (ms) under the active environment's coefficients for the given language.

Source code in rbx/box/environment.py
def compute_walltime(cpu_tl_ms: int, language: Optional[str]) -> int:
    """Computes the wall-time limit (ms) for a CPU time limit (ms) under the active environment's coefficients for the given language."""
    return apply_walltime_formula(cpu_tl_ms, get_walltime_coeffs_for_language(language))

get_walltime_coeffs_for_language(language) #

Reads the active environment and resolves wall-time coefficients for the given language name (None -> environment defaults).

Source code in rbx/box/environment.py
def get_walltime_coeffs_for_language(
    language: Optional[str],
) -> Tuple[float, int]:
    """Reads the active environment and resolves wall-time coefficients for the
    given language name (None -> environment defaults)."""
    env = get_environment()
    lang = get_language_or_nil(language) if language is not None else None
    return resolve_walltime_coeffs(env.timing, lang)

is_interpreted(language, solution=False) #

Whether the language runs its source directly (the compilable is the executable) rather than producing a separate binary. This is exactly the signal compile_item uses to take the passthrough path.

Source code in rbx/box/environment.py
def is_interpreted(language: str, solution: bool = False) -> bool:
    """Whether the language runs its source directly (the compilable is the
    executable) rather than producing a separate binary. This is exactly the
    signal ``compile_item`` uses to take the passthrough path."""
    config = get_compilation_config(language, solution)
    return bool(config.passthrough) or not config.commands

language_kinds(language) #

The set of :class:LanguageKind a language belongs to, derived from its toolchain: the compilation commands if it compiles, else its execution command. Deriving from the actual compiler/interpreter (not the language name) keeps dispatch robust to custom language names.

Source code in rbx/box/environment.py
def language_kinds(language: EnvironmentLanguage) -> Set[LanguageKind]:
    """The set of :class:`LanguageKind` a language belongs to, derived from its
    toolchain: the compilation commands if it compiles, else its execution command.
    Deriving from the actual compiler/interpreter (not the language *name*) keeps
    dispatch robust to custom language names."""
    if language.compilation and language.compilation.commands:
        commands = list(language.compilation.commands)
    elif language.execution and language.execution.command:
        commands = [language.execution.command]
    else:
        commands = []

    kinds: Set[LanguageKind] = set()
    for command in commands:
        parts = shlex.split(command)
        kinds |= command_kinds(parts[0] if parts else command)
    return kinds

resolve_walltime_coeffs(timing, language) #

Resolves the effective (wall_time_multiplier, wall_time_increment_ms), where a per-language override takes precedence field-by-field over the environment-level timing defaults.

Source code in rbx/box/environment.py
def resolve_walltime_coeffs(
    timing: TimingConfig,
    language: Optional[EnvironmentLanguage],
) -> Tuple[float, int]:
    """Resolves the effective (wall_time_multiplier, wall_time_increment_ms),
    where a per-language override takes precedence field-by-field over the
    environment-level timing defaults."""
    multiplier = timing.wallTimeMultiplier
    increment = timing.wallTimeIncrement
    if language is not None and language.timing is not None:
        if language.timing.wallTimeMultiplier is not None:
            multiplier = language.timing.wallTimeMultiplier
        if language.timing.wallTimeIncrement is not None:
            increment = language.timing.wallTimeIncrement
    return multiplier, increment

Extensions#

Extensions #

Bases: BaseModel

Parameters:

Name Type Description Default
boca BocaExtension | None

Environment-level extensions for BOCA packaging.

None
Source code in rbx/box/extensions.py
class Extensions(BaseModel):
    boca: Optional[BocaExtension] = Field(
        default=None, description='Environment-level extensions for BOCA packaging.'
    )

LanguageExtensions #

Bases: BaseModel

Parameters:

Name Type Description Default
boca BocaLanguageExtension | None

Language-level extensions for BOCA packaging.

None
polygon PolygonLanguageExtension | None

Language-level extensions for Polygon packaging.

None
Source code in rbx/box/extensions.py
class LanguageExtensions(BaseModel):
    boca: Optional[BocaLanguageExtension] = Field(
        default=None, description='Language-level extensions for BOCA packaging.'
    )

    polygon: Optional[PolygonLanguageExtension] = Field(
        default=None, description='Language-level extensions for Polygon packaging.'
    )

BOCA#

BocaExtension #

Bases: RejectsRemovedFields

Parameters:

Name Type Description Default
flags Dict[Literal[c, cpp, cc, kt, java, py2, py3], str]
{}
minRunningTime int | None
None
preferContestLetter bool
False
usePypy bool
False
languages List[str] | None
None
maximumTimeError float | None
None
Source code in rbx/box/packaging/boca/extension.py
class BocaExtension(RejectsRemovedFields):
    model_config = ConfigDict(extra='forbid')

    flags: typing.Dict[BocaLanguage, str] = {}
    # Optional floor (in milliseconds) on the TOTAL BOCA time budget. When set, the
    # solution is run ceil(minRunningTime / timeLimit) times so the accumulated budget
    # reaches this floor, while the effective per-run TL stays exactly equal to the real TL.
    minRunningTime: typing.Optional[int] = Field(default=None, gt=0)
    preferContestLetter: bool = False
    usePypy: bool = False

    # Removed in rbx v1 (see the "Migrating to rbx v1" troubleshooting guide).
    languages: typing.Annotated[typing.Optional[typing.List[str]], Removed()] = Field(
        default=None,
        deprecated=(
            'Env-level `extensions.boca.languages` was removed in rbx v1. Declare '
            '`languages` per rbx language under its `extensions.boca` instead; the '
            "emitted set is the union of every language's `languages`."
        ),
    )
    maximumTimeError: typing.Annotated[typing.Optional[float], Removed()] = Field(
        default=None,
        deprecated=(
            '`maximumTimeError` was removed in rbx v1. It has been ignored since #494 '
            '(rbx emits exact fractional time limits). Use `minRunningTime` instead.'
        ),
    )

    def flags_with_defaults(self) -> typing.Dict[BocaLanguage, str]:
        res: typing.Dict[BocaLanguage, str] = {
            'c': '-std=gnu11 -O2 -lm -static',
            'cpp': '-std=c++20 -O2 -lm -static',
            'cc': '-std=c++20 -O2 -lm -static',
        }
        res.update(self.flags)
        return res

BocaLanguageExtension #

Bases: RejectsRemovedFields

Parameters:

Name Type Description Default
languages List[str] | None
None
template str | None
None
bocaLanguage str | None
None
Source code in rbx/box/packaging/boca/extension.py
class BocaLanguageExtension(RejectsRemovedFields):
    model_config = ConfigDict(extra='forbid')

    # BOCA languages this rbx language maps to. The first entry is the canonical/primary
    # one, used as the forward (rbx -> BOCA) mapping. Every entry is emitted as a separate
    # per-language script dir in the BOCA package (e.g. ['cc', 'cpp'] emits both).
    languages: typing.Optional[typing.List[str]] = None
    # On-disk BOCA template dir (under rbx/resources/packagers/boca/{compile,run,
    # interactive}/) to source per-language scripts from. Required whenever `languages`
    # is set.
    template: typing.Optional[str] = None

    # Removed in rbx v1 (see the "Migrating to rbx v1" troubleshooting guide).
    bocaLanguage: typing.Annotated[typing.Optional[str], Removed()] = Field(
        default=None,
        deprecated=(
            '`bocaLanguage` was removed in rbx v1. Use `languages` (a list) instead, '
            'with an explicit `template`.'
        ),
    )

    @model_validator(mode='after')
    def _require_template_with_languages(self) -> 'BocaLanguageExtension':
        if self.languages and not self.template:
            raise ValueError(
                'A `template` is required when `languages` is set on a BOCA language '
                'extension. Set `template` to one of the on-disk template dirs '
                '(c, cc, cpp, java, kt, py2, py3).'
            )
        return self

    @property
    def resolved_languages(self) -> typing.List[str]:
        return self.languages or []

    @property
    def primary_language(self) -> typing.Optional[str]:
        langs = self.resolved_languages
        return langs[0] if langs else None

    @property
    def resolved_template(self) -> typing.Optional[str]:
        return self.template