{"id":16538752,"url":"https://github.com/spyoungtech/behave-problem-example","last_synced_at":"2026-05-22T16:37:13.425Z","repository":{"id":146395428,"uuid":"121764098","full_name":"spyoungtech/behave-problem-example","owner":"spyoungtech","description":"A temporary repository to demonstrate a problem.","archived":false,"fork":false,"pushed_at":"2018-02-16T21:36:42.000Z","size":6,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-14T06:16:11.507Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/spyoungtech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-02-16T15:07:11.000Z","updated_at":"2018-02-21T21:31:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"be7e247f-e4fd-4292-b7da-07268b2c671d","html_url":"https://github.com/spyoungtech/behave-problem-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spyoungtech%2Fbehave-problem-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spyoungtech%2Fbehave-problem-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spyoungtech%2Fbehave-problem-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/spyoungtech%2Fbehave-problem-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/spyoungtech","download_url":"https://codeload.github.com/spyoungtech/behave-problem-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241763751,"owners_count":20016162,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T18:46:42.015Z","updated_at":"2026-05-22T16:37:13.398Z","avatar_url":"https://github.com/spyoungtech.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# What is this?\nThis is, in part, a springboard for creating guidelines and best-practices for behave step library writers. For the \nmoment, this only briefly describes some underlying ideas and concepts while the bulk explains a problem \nthat arises in designing a step library and provides a long-winded narrative of arriving at possible solutions.\n\nFor the purposes of this example `another_lib` is provided in the working directory so the example is executable, \nhowever in practice the idea is this library would be installed to the users site-packages.\n\n\n## Premise\n\nThe idea behind this premise is to provide behave users a beautiful API for using step libraries. This rests on some \nassumptions about how step library writers 'ought to provide step definitions and certain freedoms \nusers of those libraries should have. Here's the top five that come to mind:\n\n- Users of step libraries should be able to import the provided step definitions directly into their own step files. \n- Users may use multiple step libraries\n- Users should be able to easily and cleanly modify behaviors of the step library.\n- Authors of large libraries may provide namespaced step definitions to allow importing just a subset of all the library steps, I.E. \n`step_library.domain.steps` could be imported and registered without also registering `step_library.other_domain.steps`\n- It's hard to extend module-level functions. As such, it's ideal if the definitions in a step library delegate as much work as possible to user-extendable interfaces elsewhere in the library.\n\n\nThe step library I author, [behave-webdriver](https://github.com/spyoungtech/behave-webdriver), (a work-in-progress) \nis designed with these ideas in mind.\n\n\n# A key problem with importable step definitions\n\nThe behave matcher uses global state. As such, a library writer can unexpectedly modify this \nglobal state to the detriment of the user **and vice versa**.\n\nDespite the fact that behave [attempts to manage this problem of gloabl state](https://github.com/behave/behave/blob/master/behave/runner_util.py#L389-L413) \nwhen loading the step modules, this does not fix the problem of global state being modified when the step module \nimports a step library to register steps.\n\nIn light of this, library writers must bear the burden of carefully managing this global state \nto avoid altering how user code would otherwise behave.\n\n[About global state](https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil).\n\n\n# The problem demonstrated\n\n\nThe master branch is in state where the tests of `my_package` are broken because of global state changed by `another_lib`.  \nRemember that `another_lib` is only provided directly in the repository as a convenience; \nthe idea is that step libraries would be contained in the user's installed packages.\n\nThe only thing you need to run this is behave v1.2.5\n\n\n## The results\n\n```\n$ behave\n\nmysteps runtime matcher:  \u003cclass 'behave.matchers.RegexMatcher'\u003e\n\nFeature: use a step library # features\\example.feature:1\n  As a behave user\n  I want to use step implementations written by others in combination with my own.\n  Scenario: I can use the step library steps just by importing them  # features\\example.feature:5\n    Given I use a step library                                       # another_lib\\steps.py:5\n\n  Scenario: My standalone steps should work the same regardless of whether I use imported a step library  # features\\example.feature:8\n    Then I expect that return_foo returns \"foo\"                                                           # None\n\n  Scenario: I can use a step library along with my own steps, even if they don't use my matcher  # features\\example.feature:11\n    Given I use a step library                                                                   # another_lib\\steps.py:5\n    Then I expect that return_foo returns \"foo\"                                                  # None\n\n  Scenario: The matcher is not changed importing a step library (meta)  # features\\example.feature:15\n    Given I use a step library                                          # another_lib\\steps.py:5\n    Then I expect the runtime matcher is \"parse\"                        # features\\steps\\mysteps.py:20\n      Assertion Failed: Expected the matcher was parse but it was re\n\n\n\nFailing scenarios:\n  features\\example.feature:8  My standalone steps should work the same regardless of whether I use imported a step library\n  features\\example.feature:11  I can use a step library along with my own steps, even if they don't use my matcher\n  features\\example.feature:15  The matcher is not changed importing a step library (meta)\n\n0 features passed, 1 failed, 0 skipped\n1 scenario passed, 3 failed, 0 skipped\n3 steps passed, 1 failed, 0 skipped, 2 undefined\nTook 0m0.001s\n\nYou can implement step definitions for undefined steps with these snippets:\n\n@then(u'I expect that return_foo returns \"foo\"')\ndef step_impl(context):\n    raise NotImplementedError(u'STEP: Then I expect that return_foo returns \"foo\"')\n\n```\n\n### Breaking it down\n\nThe first thing you'll notice is the debug print showing that the matcher was the `RegexMatcher`, which was indeed set \nby the step library.\n\nThis means that the user-provided step definition\n```python\n@then(u'I expect that return_foo returns \"{expected}\"')\n```\nwill not match, therefore behave assumes the step in the feature file is undefined, so the first two scenarios fail. \nAlso notice we identify that the matcher was, in fact, not the expected 'parse', but 're'\n\n```\n  Scenario: The matcher is not changed by misbehaving library (meta)  # features\\example.feature:12\n    Given I use a misbehaving library                                 # another_lib\\steps.py:5\n    Then I expect the runtime matcher is \"parse\"                      # features\\steps\\mysteps.py:20\n      Assertion Failed: Expected the matcher was parse but it was re\n```\n\n\n# Proposed solution\n\nThe burden of solving this problem (in this authors view) should lie squarely on the shoulders of step library writers. \n\nStep library writers should attempt to return the global state to its previous state prior to the library import. \nThe basic idea is as follows.\n\n- identify the matcher in use before modifying the matcher\n- Always explicitly set the matcher they use, because a user (or basically any package) could have modified the global state before importing the library steps\n- at the end of each step module, set the matcher to the matcher that was previously identified\n\n\n\n## Obstacles\n\n- The only 'public' interface for altering the matcher is `use_step_matcher`.\n- There is no public interface for identifying the *name* of the current matcher for use with `use_step_matcher`\n\n## Possible Solutions: a progression of ideas\n\n\n### Band-aid\n\nTo get around this, the current method is to set the matcher to the default by calling `use_step_matcher('parse')` at the end of \nstep modules in the step library. This strictly uses behave's public interfaces to attempt to alleviate the problem.\n\n```python\n# site-packages/step_library/somewhere/some_steps.py\nfrom behave import *\n# always set the matcher explicitly\nuse_step_matcher('some_matcher') # we can never be confident of the global state to begin with.\n\n# write step definitions here\n\n# set the matcher back to the default at the end of the module\nuse_step_matcher('parse') # perhaps there's a way to identify the default module if changed in user's environment.py\n```\n\nHowever, this is not entirely adequate because the user may not necesarily be using the parse matcher at the time \nthe step library is imported.\n\n\n### Band-aid+\n\nInstead of blindly setting the matcher back to the parse matcher, we can try to figure out which matcher was being used \nwhen our step library was imported so we can set it back later at the end of our module. Though, this means we need to \ndig a little deeper into behave's API.\n \nThe current matcher *class* can be retrieved via inspecting the global state `behave.matchers.current_matcher`. \nUnfortunately, there's no concrete way to get a string out of the matcher class to use with `use_step_matcher` \nWe can, however, use our knowledge of the matchers provided by behave to at least cover those.\n\nThe following is an example of a function that does this\n```python\n# ...step_library/utils.py\nimport behave\ndef get_matcher_name(default=None):\n    \"\"\"\n    A hack to inspect the global state to return a name suitable for use with ``use_step_matcher`` \n    Only works if the current matcher is a matcher provided by behave.\n    \n    :param default: if the name cannot be found, return this instead. If not provided, a KeyError will be raised.\n    :raises: KeyError\n    \"\"\"\n    \n    name_map = {'ParseMatcher': 'parse',\n                'CFParseMatcher': 'cfparse',\n                'RegexMatcher': 're'}\n    matcher_class = behave.matchers.current_matcher\n    class_name = matcher_class.__name__\n    matcher_name = name_map.get(class_name, default)\n    if matcher_name is None:\n        raise KeyError('Could not determine a name for class {matcher_class}')\n    return matcher_name\n```\n\nApplying this idea on top of the previous example, still only using the public `use_step_matcher` to make any *changes* to the state, we might do something like this\n\n```python\n# site-packages/step_library/somewhere/some_steps.py\nfrom behave import *\nfrom step_library.utils import get_matcher_name\n\n# Before changing anything, check the global state and identify the original matcher by name.\noriginal_matcher_name = get_matcher_name()\n\n\n# always set the matcher explicitly\nuse_step_matcher('some_matcher') # we can never be confident of the global state to begin with.\n\n# write your step definitions here\n\n# set the matcher back to the original\nuse_step_matcher(original_matcher_name)\n```\n\nSo, we increased the complexity of our solution somewhat by relying on some of behave's internal API, and we gained \nsome resiliency and covered cases for those using a behave-provided matcher, and we still only use behave's public \ninterfaces for *modifying* the global state. \n\nHowever, this is still incomplete if we consider users who may use a custom matcher.\n\n### Complete* solution\n\nFollowing the same idea, we can cover custom matchers too. Instead of getting the name from the current matcher, \nwe simply get the matcher class then, at the end of the module, monkey patch the global state back to the original matcher \nafter the step library steps are registered. \n\nThis ensures users will not have any surprises by importing the library.\n\n```python\n# some_library/steps/libsteps.py\nfrom behave import *\nimport behave\n\n# save the original matcher before doing anything else\noriginal_matcher = behave.matchers.current_matcher\n\n# always do this, because the matcher may have been changed by a user or misbehaving library\nuse_step_matcher('some_matcher') \n\n# write step definitions here\n\n# restore the original matcher at the end of the module by modifying the global state\nbehave.matchers.current_matcher = original_matcher # not ideal, but it works\n```\n\n*There is a cost here, however, because we are taking global state management into our own hands through a \nless-than-public interface. So, we risk the fact that an implementation change in `behave` can break this code, possibly \nwith little to no warning. If you take this approach, you, as a behave superuser, are encouraged to watch the \n[behave](https://github.com/behave/behave) project closely for upcoming changes and further, contribute to changes that \nmake the behave ecosystem more flexible for its extenders.\n\n## New results with any of these solutions applied\n\nIf you apply the changes (available in the `solution-applied` branch) you'll see that we address the problem for the user \nand all the tests pass.\n\n```\n$ git checkout solution-applied\n$ behave\n\nmysteps runtime matcher:  \u003cclass 'behave.matchers.ParseMatcher'\u003e\n\nFeature: use a step library # features\\example.feature:1\n  As a behave user\n  I want to use step implementations written by others in combination with my own.\n  Scenario: I can use the step library steps just by importing them  # features\\example.feature:5\n    Given I use a step library                                       # another_lib\\steps.py:6\n\n  Scenario: My standalone steps should work the same regardless of whether I use imported a step library  # features\\example.feature:8\n    Then I expect that return_foo returns \"foo\"                                                           # features\\steps\\mysteps.py:12\n\n  Scenario: I can use a step library along with my own steps, even if they don't use my matcher  # features\\example.feature:11\n    Given I use a step library                                                                   # another_lib\\steps.py:6\n    Then I expect that return_foo returns \"foo\"                                                  # features\\steps\\mysteps.py:12\n\n  Scenario: The matcher is not changed importing a step library (meta)  # features\\example.feature:15\n    Given I use a step library                                          # another_lib\\steps.py:6\n    Then I expect the runtime matcher is \"parse\"                        # features\\steps\\mysteps.py:20\n\n1 feature passed, 0 failed, 0 skipped\n4 scenarios passed, 0 failed, 0 skipped\n6 steps passed, 0 failed, 0 skipped, 0 undefined\nTook 0m0.001s\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspyoungtech%2Fbehave-problem-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fspyoungtech%2Fbehave-problem-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fspyoungtech%2Fbehave-problem-example/lists"}