Cardinality: Zero or One (Optional)

There are some cases, when a text part may be present or not. Therefore, this text part is an optional and has cardinality zero or one (0..1).

The parse_type.TypeBuilder can be used to compute the type with cardinality zero or one based on data type with cardinality one.

See also

Use Optional Part in Step Definitions for a simpler solution to this problem by using the cardinality field in parse expressions.

Feature Example

Assuming you want to write something like this:

# file:datatype.features/cardinality.zero_or_one.feature
Feature: Data Type with Cardinality 0..1 (Optional Part)

  Scenario: Case1 "When attacked by a ..."
    Given the ninja has a black-belt
    When attacked by a samurai

  Scenario: Case2 "When attacked by ..."
    Given the ninja has a black-belt
    When attacked by Chuck Norris

# -- DESCRIPTION:
# "When attacked by ...": Once with "a ", once without it.
# Only one step should be used.

Define the Data Type

# file:datatype.features/steps/step_cardinality_zero_or_one.py
# ------------------------------------------------------------------------
# USER-DEFINED TYPES:
# ------------------------------------------------------------------------
from behave import register_type
from parse_type import TypeBuilder
import parse

@parse.with_pattern(r"a\s+")
def parse_word_a(text):
    """Type converter for "a " (followed by one/more spaces)."""
    return text.strip()

# -- SAME:
# parse_optional_word_a = TypeBuilder.with_zero_or_one(parse_word_a)
parse_optional_word_a   = TypeBuilder.with_optional(parse_word_a)
register_type(optional_a_=parse_optional_word_a)


Note

The TypeBuilder.with_optional() function performs the magic. It computes a regular expression pattern for the given choice of words/strings and stores them in parse_optional_word_a.pattern attribute.

Provide the Step Definitions

# file:datatype.features/steps/step_cardinality_zero_or_one.py
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then
from hamcrest import assert_that, equal_to, is_in

# -- OPTIONAL-PART: {:optional_a_}
# By using data type with cardinality zero or one (0..1, optional).
@when('attacked by {:optional_a_}{opponent}')
def step_attacked_by(context, a_, opponent):
    context.ninja_fight.opponent = opponent
    # -- VERIFY: Optional part feature.
    assert_that(a_, is_in(["a", None]))
    assert_that(opponent, is_in(["Chuck Norris", "samurai"]))

# ----------------------------------------------------------------------------
# MORE STEPS:
# ----------------------------------------------------------------------------
from ninja_fight import NinjaFight

@given('the ninja has a {achievement_level}')
def step_the_ninja_has_a(context, achievement_level):
    context.ninja_fight = NinjaFight(achievement_level)

@then('the ninja should {reaction}')
def step_the_ninja_should(context, reaction):
    assert_that(reaction, equal_to(context.ninja_fight.decision()))

Run the Test

Now we run this example with behave:

$ behave ../datatype.features/cardinality.zero_or_one.feature
Feature: Data Type with Cardinality 0..1 (Optional Part)   # ../datatype.features/cardinality.zero_or_one.feature:1

  Scenario: Case1 "When attacked by a ..."  # ../datatype.features/cardinality.zero_or_one.feature:3
    Given the ninja has a black-belt        # ../datatype.features/steps/step_cardinality_zero_or_one.py:54
    When attacked by a samurai              # ../datatype.features/steps/step_cardinality_zero_or_one.py:42

  Scenario: Case2 "When attacked by ..."  # ../datatype.features/cardinality.zero_or_one.feature:7
    Given the ninja has a black-belt      # ../datatype.features/steps/step_cardinality_zero_or_one.py:54
    When attacked by Chuck Norris         # ../datatype.features/steps/step_cardinality_zero_or_one.py:42

1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
4 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.002s

The Complete Picture

# file:datatype.features/steps/step_cardinality_zero_or_one.py
# -*- coding: UTF-8 -*-
"""
Feature: Use Optional Part in Step Definitions

  Scenario: Case 1 with "a "
    Given ...
    When attacked by a samurai

  Scenario: Case 2 without "a "
    Given ...
    When attacked by Chuck Norris
"""

# @mark.user_defined_types
# ------------------------------------------------------------------------
# USER-DEFINED TYPES:
# ------------------------------------------------------------------------
from behave import register_type
from parse_type import TypeBuilder
import parse

@parse.with_pattern(r"a\s+")
def parse_word_a(text):
    """Type converter for "a " (followed by one/more spaces)."""
    return text.strip()

# -- SAME:
# parse_optional_word_a = TypeBuilder.with_zero_or_one(parse_word_a)
parse_optional_word_a   = TypeBuilder.with_optional(parse_word_a)
register_type(optional_a_=parse_optional_word_a)


# @mark.steps
# ----------------------------------------------------------------------------
# STEPS:
# ----------------------------------------------------------------------------
from behave import given, when, then
from hamcrest import assert_that, equal_to, is_in

# -- OPTIONAL-PART: {:optional_a_}
# By using data type with cardinality zero or one (0..1, optional).
@when('attacked by {:optional_a_}{opponent}')
def step_attacked_by(context, a_, opponent):
    context.ninja_fight.opponent = opponent
    # -- VERIFY: Optional part feature.
    assert_that(a_, is_in(["a", None]))
    assert_that(opponent, is_in(["Chuck Norris", "samurai"]))

# ----------------------------------------------------------------------------
# MORE STEPS:
# ----------------------------------------------------------------------------
from ninja_fight import NinjaFight

@given('the ninja has a {achievement_level}')
def step_the_ninja_has_a(context, achievement_level):
    context.ninja_fight = NinjaFight(achievement_level)

@then('the ninja should {reaction}')
def step_the_ninja_should(context, reaction):
    assert_that(reaction, equal_to(context.ninja_fight.decision()))