Merge branch 'remove_unused_broken_flake8_listen' into 'master'

Remove unused and broken flake8.listen plugin type

Closes #480

See merge request pycqa/flake8!274
This commit is contained in:
Ian Stapleton Cordasco 2018-12-28 15:39:49 +00:00
commit 7f50c3acc4
11 changed files with 19 additions and 525 deletions

View file

@ -4,15 +4,13 @@ Plugin Handling
Plugin Management
-----------------
|Flake8| 3.0 added support for two other plugins besides those which define
|Flake8| 3.0 added support for other plugins besides those which define
new checks. It now supports:
- extra checks
- alternative report formatters
- listeners to auto-correct violations of checks
To facilitate this, |Flake8| needed a more mature way of managing plugins.
Thus, we developed the |PluginManager| which accepts a namespace and will load
the plugins for that namespace. A |PluginManager| creates and manages many
@ -38,12 +36,10 @@ the |PTM| will subclass it and specify the ``namespace``, e.g.,
This provides a few extra methods via the |PluginManager|'s ``map`` method.
Finally, we create three classes of plugins:
Finally, we create two classes of plugins:
- :class:`~flake8.plugins.manager.Checkers`
- :class:`~flake8.plugins.manager.Listeners`
- :class:`~flake8.plugins.manager.ReportFormatters`
These are used to interact with each of the types of plugins individually.
@ -53,29 +49,6 @@ These are used to interact with each of the types of plugins individually.
Our inspiration for our plugin handling comes from the author's extensive
experience with ``stevedore``.
Notifying Listener Plugins
--------------------------
One of the interesting challenges with allowing plugins to be notified each
time an error or warning is emitted by a checker is finding listeners quickly
and efficiently. It makes sense to allow a listener to listen for a certain
class of warnings or just a specific warning. Hence, we need to allow all
plugins that listen to a specific warning or class to be notified. For
example, someone might register a listener for ``E1`` and another for ``E111``
if ``E111`` is triggered by the code, both listeners should be notified.
If ``E112`` is returned, then only ``E1`` (and any other listeners) would be
notified.
To implement this goal, we needed an object to store listeners in that would
allow for efficient look up - a Trie (or Prefix Tree). Given that none of the
existing packages on PyPI allowed for storing data on each node of the trie,
it was left up to write our own as :class:`~flake8.plugins._trie.Trie`. On
top of that we layer our :class:`~flake8.plugins.notifier.Notifier` class.
Now when |Flake8| receives an error or warning, we can easily call the
:meth:`~flake8.plugins.notifier.Notifier.notify` method and let plugins act on
that knowledge.
Default Plugins
---------------
@ -103,7 +76,7 @@ API Documentation
.. autoclass:: flake8.plugins.manager.PluginManager
:members:
:special-members: __init__, __contains__, __getitem__
:special-members: __init__
.. autoclass:: flake8.plugins.manager.Plugin
:members:
@ -115,15 +88,8 @@ API Documentation
.. autoclass:: flake8.plugins.manager.Checkers
:members:
.. autoclass:: flake8.plugins.manager.Listeners
:members: build_notifier
.. autoclass:: flake8.plugins.manager.ReportFormatters
.. autoclass:: flake8.plugins.notifier.Notifier
.. autoclass:: flake8.plugins._trie.Trie
.. |PluginManager| replace:: :class:`~flake8.plugins.manager.PluginManager`
.. |Plugin| replace:: :class:`~flake8.plugins.manager.Plugin`
.. |PTM| replace:: :class:`~flake8.plugins.manager.PluginTypeManager`

View file

@ -86,13 +86,10 @@ grouping of entry-points that flake8 should look in.
- ``flake8.extension``
- ``flake8.listen``
- ``flake8.report``
If your plugin is one that adds checks to |Flake8|, you will use
``flake8.extension``. If your plugin automatically fixes errors in code, you
will use ``flake8.listen``. Finally, if your plugin performs extra report
``flake8.extension``. If your plugin performs extra report
handling (formatting, filtering, etc.) it will use ``flake8.report``.
If our ``ExamplePlugin`` is something that adds checks, our code would look

View file

@ -57,14 +57,10 @@ class Application(object):
self.local_plugins = None
#: The instance of :class:`flake8.plugins.manager.Checkers`
self.check_plugins = None
#: The instance of :class:`flake8.plugins.manager.Listeners`
self.listening_plugins = None
#: The instance of :class:`flake8.plugins.manager.ReportFormatters`
self.formatting_plugins = None
#: The user-selected formatter from :attr:`formatting_plugins`
self.formatter = None
#: The :class:`flake8.plugins.notifier.Notifier` for listening plugins
self.listener_trie = None
#: The :class:`flake8.style_guide.StyleGuideManager` built from the
#: user's options
self.guide = None
@ -166,11 +162,11 @@ class Application(object):
# type: () -> NoneType
"""Find and load the plugins for this application.
If :attr:`check_plugins`, :attr:`listening_plugins`, or
:attr:`formatting_plugins` are ``None`` then this method will update
them with the appropriate plugin manager instance. Given the expense
of finding plugins (via :mod:`entrypoints`) we want this to be
idempotent and so only update those attributes if they are ``None``.
If :attr:`check_plugins`, or :attr:`formatting_plugins` are ``None``
then this method will update them with the appropriate plugin manager
instance. Given the expense of finding plugins (via :mod:`entrypoints`)
we want this to be idempotent and so only update those attributes if
they are ``None``.
"""
if self.local_plugins is None:
self.local_plugins = config.get_local_plugins(
@ -186,16 +182,12 @@ class Application(object):
self.local_plugins.extension
)
if self.listening_plugins is None:
self.listening_plugins = plugin_manager.Listeners()
if self.formatting_plugins is None:
self.formatting_plugins = plugin_manager.ReportFormatters(
self.local_plugins.report
)
self.check_plugins.load_plugins()
self.listening_plugins.load_plugins()
self.formatting_plugins.load_plugins()
def register_plugin_options(self):
@ -203,7 +195,6 @@ class Application(object):
"""Register options provided by plugins to our option manager."""
self.check_plugins.register_options(self.option_manager)
self.check_plugins.register_plugin_versions(self.option_manager)
self.listening_plugins.register_options(self.option_manager)
self.formatting_plugins.register_options(self.option_manager)
def parse_configuration_and_cli(self, argv=None):
@ -229,9 +220,6 @@ class Application(object):
self.check_plugins.provide_options(
self.option_manager, self.options, self.args
)
self.listening_plugins.provide_options(
self.option_manager, self.options, self.args
)
self.formatting_plugins.provide_options(
self.option_manager, self.options, self.args
)
@ -264,18 +252,12 @@ class Application(object):
self.formatter = formatter_class(self.options)
def make_notifier(self):
# type: () -> NoneType
"""Initialize our listener Notifier."""
if self.listener_trie is None:
self.listener_trie = self.listening_plugins.build_notifier()
def make_guide(self):
# type: () -> NoneType
"""Initialize our StyleGuide."""
if self.guide is None:
self.guide = style_guide.StyleGuideManager(
self.options, self.listener_trie, self.formatter
self.options, self.formatter
)
if self.running_against_diff:
@ -373,7 +355,6 @@ class Application(object):
self.register_plugin_options()
self.parse_configuration_and_cli(argv)
self.make_formatter()
self.make_notifier()
self.make_guide()
self.make_file_checker_manager()

View file

@ -1,95 +0,0 @@
"""Independent implementation of a Trie tree."""
__all__ = ("Trie", "TrieNode")
def _iterate_stringlike_objects(string):
for i in range(len(string)):
yield string[i : i + 1]
class Trie(object):
"""The object that manages the trie nodes."""
def __init__(self):
"""Initialize an empty trie."""
self.root = TrieNode(None, None)
def add(self, path, node_data):
"""Add the node data to the path described."""
node = self.root
for prefix in _iterate_stringlike_objects(path):
child = node.find_prefix(prefix)
if child is None:
child = node.add_child(prefix, [])
node = child
node.data.append(node_data)
def find(self, path):
"""Find a node based on the path provided."""
node = self.root
for prefix in _iterate_stringlike_objects(path):
child = node.find_prefix(prefix)
if child is None:
return None
node = child
return node
def traverse(self):
"""Traverse this tree.
This performs a depth-first pre-order traversal of children in this
tree. It returns the results consistently by first sorting the
children based on their prefix and then traversing them in
alphabetical order.
"""
return self.root.traverse()
class TrieNode(object):
"""The majority of the implementation details of a Trie."""
def __init__(self, prefix, data, children=None):
"""Initialize a TrieNode with data and children."""
self.children = children or {}
self.data = data
self.prefix = prefix
def __repr__(self):
"""Generate an easy to read representation of the node."""
return "TrieNode(prefix={0}, data={1})".format(self.prefix, self.data)
def find_prefix(self, prefix):
"""Find the prefix in the children of this node.
:returns: A child matching the prefix or None.
:rtype: :class:`~TrieNode` or None
"""
return self.children.get(prefix, None)
def add_child(self, prefix, data, children=None):
"""Create and add a new child node.
:returns: The newly created node
:rtype: :class:`~TrieNode`
"""
new_node = TrieNode(prefix, data, children)
self.children[prefix] = new_node
return new_node
def traverse(self):
"""Traverse children of this node.
This performs a depth-first pre-order traversal of the remaining
children in this sub-tree. It returns the results consistently by
first sorting the children based on their prefix and then traversing
them in alphabetical order.
"""
if not self.children:
return
for prefix in sorted(self.children):
child = self.children[prefix]
yield child
for child in child.traverse():
yield child

View file

@ -6,7 +6,6 @@ import entrypoints
from flake8 import exceptions
from flake8 import utils
from flake8.plugins import notifier
if sys.version_info >= (3, 3):
import collections.abc as collections_abc
@ -15,13 +14,7 @@ else:
LOG = logging.getLogger(__name__)
__all__ = (
"Checkers",
"Listeners",
"Plugin",
"PluginManager",
"ReportFormatters",
)
__all__ = ("Checkers", "Plugin", "PluginManager", "ReportFormatters")
NO_GROUP_FOUND = object()
@ -444,24 +437,6 @@ class PluginTypeManager(object):
list(self.manager.map(call_provide_options))
class NotifierBuilderMixin(object): # pylint: disable=too-few-public-methods
"""Mixin class that builds a Notifier from a PluginManager."""
def build_notifier(self):
"""Build a Notifier for our Listeners.
:returns:
Object to notify our listeners of certain error codes and
warnings.
:rtype:
:class:`~flake8.notifier.Notifier`
"""
notifier_trie = notifier.Notifier()
for name in self.names:
notifier_trie.register_listener(name, self.manager[name])
return notifier_trie
class Checkers(PluginTypeManager):
"""All of the checkers registered through entry-points or config."""
@ -542,12 +517,6 @@ class Checkers(PluginTypeManager):
return plugins
class Listeners(PluginTypeManager, NotifierBuilderMixin):
"""All of the listeners registered through entry-points or config."""
namespace = "flake8.listen"
class ReportFormatters(PluginTypeManager):
"""All of the report formatters registered through entry-points/config."""

View file

@ -1,46 +0,0 @@
"""Implementation of the class that registers and notifies listeners."""
from flake8.plugins import _trie
class Notifier(object):
"""Object that tracks and notifies listener objects."""
def __init__(self):
"""Initialize an empty notifier object."""
self.listeners = _trie.Trie()
def listeners_for(self, error_code):
"""Retrieve listeners for an error_code.
There may be listeners registered for E1, E100, E101, E110, E112, and
E126. To get all the listeners for one of E100, E101, E110, E112, or
E126 you would also need to incorporate the listeners for E1 (since
they're all in the same class).
Example usage:
.. code-block:: python
from flake8 import notifier
n = notifier.Notifier()
# register listeners
for listener in n.listeners_for('W102'):
listener.notify(...)
"""
path = error_code
while path:
node = self.listeners.find(path)
listeners = getattr(node, "data", [])
for listener in listeners:
yield listener
path = path[:-1]
def notify(self, error_code, *args, **kwargs):
"""Notify all listeners for the specified error code."""
for listener in self.listeners_for(error_code):
listener.notify(error_code, *args, **kwargs)
def register_listener(self, error_code, listener):
"""Register a listener for a specific error_code."""
self.listeners.add(error_code, listener)

View file

@ -325,19 +325,18 @@ class DecisionEngine(object):
class StyleGuideManager(object):
"""Manage multiple style guides for a single run."""
def __init__(self, options, listener_trie, formatter, decider=None):
def __init__(self, options, formatter, decider=None):
"""Initialize our StyleGuide.
.. todo:: Add parameter documentation.
"""
self.options = options
self.listener = listener_trie
self.formatter = formatter
self.stats = statistics.Statistics()
self.decider = decider or DecisionEngine(options)
self.style_guides = []
self.default_style_guide = StyleGuide(
options, listener_trie, formatter, decider=decider
options, formatter, decider=decider
)
self.style_guides = list(
itertools.chain(
@ -435,15 +434,12 @@ class StyleGuideManager(object):
class StyleGuide(object):
"""Manage a Flake8 user's style guide."""
def __init__(
self, options, listener_trie, formatter, filename=None, decider=None
):
def __init__(self, options, formatter, filename=None, decider=None):
"""Initialize our StyleGuide.
.. todo:: Add parameter documentation.
"""
self.options = options
self.listener = listener_trie
self.formatter = formatter
self.stats = statistics.Statistics()
self.decider = decider or DecisionEngine(options)
@ -461,9 +457,7 @@ class StyleGuide(object):
filename = filename or self.filename
options = copy.deepcopy(self.options)
options.ignore.extend(extend_ignore_with or [])
return StyleGuide(
options, self.listener, self.formatter, filename=filename
)
return StyleGuide(options, self.formatter, filename=filename)
@contextlib.contextmanager
def processing_file(self, filename):
@ -565,7 +559,6 @@ class StyleGuide(object):
):
self.formatter.handle(error)
self.stats.record(error)
self.listener.notify(error.code, error)
return 1
return 0

View file

@ -1,54 +0,0 @@
"""Unit tests for the Notifier object."""
import pytest
from flake8.plugins import notifier
class _Listener(object):
def __init__(self, error_code):
self.error_code = error_code
self.was_notified = False
def notify(self, error_code, *args, **kwargs):
assert error_code.startswith(self.error_code)
self.was_notified = True
class TestNotifier(object):
"""Notifier unit tests."""
@pytest.fixture(autouse=True)
def setup(self):
"""Set up each TestNotifier instance."""
self.notifier = notifier.Notifier()
self.listener_map = {}
def add_listener(error_code):
listener = _Listener(error_code)
self.listener_map[error_code] = listener
self.notifier.register_listener(error_code, listener)
for i in range(10):
add_listener('E{0}'.format(i))
for j in range(30):
add_listener('E{0}{1:02d}'.format(i, j))
def test_notify(self):
"""Show that we notify a specific error code."""
self.notifier.notify('E111', 'extra', 'args')
assert self.listener_map['E111'].was_notified is True
assert self.listener_map['E1'].was_notified is True
@pytest.mark.parametrize('code', ['W123', 'W12', 'W1', 'W'])
def test_no_listeners_for(self, code):
"""Show that we return an empty list of listeners."""
assert list(self.notifier.listeners_for(code)) == []
@pytest.mark.parametrize('code,expected', [
('E101', ['E101', 'E1']),
('E211', ['E211', 'E2']),
])
def test_listeners_for(self, code, expected):
"""Verify that we retrieve the correct listeners."""
assert ([l.error_code
for l in self.notifier.listeners_for(code)] == expected)

View file

@ -203,31 +203,3 @@ def test_proxies_getitem_to_managers_plugins_dict(PluginManager): # noqa: N803
for i in range(8):
key = 'T10%i' % i
assert type_mgr[key] is plugins[key]
class FakePluginTypeManager(manager.NotifierBuilderMixin):
"""Provide an easy way to test the NotifierBuilderMixin."""
def __init__(self, manager):
"""Initialize with our fake manager."""
self.names = sorted(manager)
self.manager = manager
@pytest.fixture
def notifier_builder():
"""Create a fake plugin type manager."""
return FakePluginTypeManager(manager={
'T100': object(),
'T101': object(),
'T110': object(),
})
def test_build_notifier(notifier_builder):
"""Verify we properly build a Notifier object."""
notifier = notifier_builder.build_notifier()
for name in ('T100', 'T101', 'T110'):
assert list(notifier.listeners_for(name)) == [
notifier_builder.manager[name]
]

View file

@ -7,7 +7,6 @@ import pytest
from flake8 import style_guide
from flake8 import utils
from flake8.formatting import base
from flake8.plugins import notifier
def create_options(**kwargs):
@ -22,35 +21,10 @@ def create_options(**kwargs):
return optparse.Values(kwargs)
@pytest.mark.parametrize('select_list,ignore_list,error_code', [
(['E111', 'E121'], [], 'E111'),
(['E111', 'E121'], [], 'E121'),
(['E11', 'E121'], ['E1'], 'E112'),
(['E41'], ['E2', 'E12', 'E4'], 'E410'),
])
def test_handle_error_notifies_listeners(select_list, ignore_list, error_code):
"""Verify that error codes notify the listener trie appropriately."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
guide = style_guide.StyleGuide(create_options(select=select_list,
ignore=ignore_list),
listener_trie=listener_trie,
formatter=formatter)
with mock.patch('linecache.getline', return_value=''):
guide.handle_error(error_code, 'stdin', 1, 0, 'error found')
error = style_guide.Violation(
error_code, 'stdin', 1, 1, 'error found', None)
listener_trie.notify.assert_called_once_with(error_code, error)
formatter.handle.assert_called_once_with(error)
def test_handle_error_does_not_raise_type_errors():
"""Verify that we handle our inputs better."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
guide = style_guide.StyleGuide(create_options(select=['T111'], ignore=[]),
listener_trie=listener_trie,
formatter=formatter)
assert 1 == guide.handle_error(
@ -58,41 +32,11 @@ def test_handle_error_does_not_raise_type_errors():
)
@pytest.mark.parametrize('select_list,ignore_list,error_code', [
(['E111', 'E121'], [], 'E122'),
(['E11', 'E12'], [], 'E132'),
(['E2', 'E12'], [], 'E321'),
(['E2', 'E12'], [], 'E410'),
(['E111', 'E121'], ['E2'], 'E122'),
(['E11', 'E12'], ['E13'], 'E132'),
(['E1', 'E3'], ['E32'], 'E321'),
(['E4'], ['E2', 'E12', 'E41'], 'E410'),
(['E111', 'E121'], [], 'E112'),
])
def test_handle_error_does_not_notify_listeners(select_list, ignore_list,
error_code):
"""Verify that error codes notify the listener trie appropriately."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
guide = style_guide.StyleGuide(create_options(select=select_list,
ignore=ignore_list),
listener_trie=listener_trie,
formatter=formatter)
with mock.patch('linecache.getline', return_value=''):
guide.handle_error(error_code, 'stdin', 1, 1, 'error found')
assert listener_trie.notify.called is False
assert formatter.handle.called is False
def test_style_guide_manager():
"""Verify how the StyleGuideManager creates a default style guide."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
options = create_options()
guide = style_guide.StyleGuideManager(options,
listener_trie=listener_trie,
formatter=formatter)
guide = style_guide.StyleGuideManager(options, formatter=formatter)
assert guide.default_style_guide.options is options
assert len(guide.style_guides) == 1
@ -114,11 +58,9 @@ PER_FILE_IGNORES_UNPARSED = [
])
def test_style_guide_applies_to(style_guide_file, filename, expected):
"""Verify that we match a file to its style guide."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
options = create_options()
guide = style_guide.StyleGuide(options,
listener_trie=listener_trie,
formatter=formatter,
filename=style_guide_file)
assert guide.applies_to(filename) is expected
@ -126,12 +68,9 @@ def test_style_guide_applies_to(style_guide_file, filename, expected):
def test_style_guide_manager_pre_file_ignores_parsing():
"""Verify how the StyleGuideManager creates a default style guide."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
options = create_options(per_file_ignores=PER_FILE_IGNORES_UNPARSED)
guide = style_guide.StyleGuideManager(options,
listener_trie=listener_trie,
formatter=formatter)
guide = style_guide.StyleGuideManager(options, formatter=formatter)
assert len(guide.style_guides) == 5
assert list(map(utils.normalize_path,
["first_file.py", "second_file.py", "third_file.py",
@ -150,14 +89,11 @@ def test_style_guide_manager_pre_file_ignores_parsing():
def test_style_guide_manager_pre_file_ignores(ignores, violation, filename,
handle_error_return):
"""Verify how the StyleGuideManager creates a default style guide."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
options = create_options(ignore=ignores,
select=['E', 'F', 'W'],
per_file_ignores=PER_FILE_IGNORES_UNPARSED)
guide = style_guide.StyleGuideManager(options,
listener_trie=listener_trie,
formatter=formatter)
guide = style_guide.StyleGuideManager(options, formatter=formatter)
assert (guide.handle_error(violation, filename, 1, 1, "Fake text")
== handle_error_return)
@ -172,12 +108,9 @@ def test_style_guide_manager_pre_file_ignores(ignores, violation, filename,
])
def test_style_guide_manager_style_guide_for(filename, expected):
"""Verify the style guide selection function."""
listener_trie = mock.create_autospec(notifier.Notifier, instance=True)
formatter = mock.create_autospec(base.BaseFormatter, instance=True)
options = create_options(per_file_ignores=PER_FILE_IGNORES_UNPARSED)
guide = style_guide.StyleGuideManager(options,
listener_trie=listener_trie,
formatter=formatter)
guide = style_guide.StyleGuideManager(options, formatter=formatter)
file_guide = guide.style_guide_for(filename)
assert file_guide.filename == expected

View file

@ -1,122 +0,0 @@
"""Unit test for the _trie module."""
from flake8.plugins import _trie as trie
class TestTrie(object):
"""Collection of tests for the Trie class."""
def test_traverse_without_data(self):
"""Verify the behaviour when traversing an empty Trie."""
tree = trie.Trie()
assert list(tree.traverse()) == []
def test_traverse_with_data(self):
"""Verify that traversal of our Trie is depth-first and pre-order."""
tree = trie.Trie()
tree.add('A', 'A')
tree.add('a', 'a')
tree.add('AB', 'B')
tree.add('Ab', 'b')
tree.add('AbC', 'C')
tree.add('Abc', 'c')
# The trie tree here should look something like
#
# <root>
# / \
# A a
# / |
# B b
# / \
# C c
#
# And the traversal should look like:
#
# A B b C c a
expected_order = ['A', 'B', 'b', 'C', 'c', 'a']
for expected, actual_node in zip(expected_order, tree.traverse()):
assert actual_node.prefix == expected
def test_find(self):
"""Exercise the Trie.find method."""
tree = trie.Trie()
tree.add('A', 'A')
tree.add('a', 'a')
tree.add('AB', 'AB')
tree.add('Ab', 'Ab')
tree.add('AbC', 'AbC')
tree.add('Abc', 'Abc')
assert tree.find('AB').data == ['AB']
assert tree.find('AbC').data == ['AbC']
assert tree.find('A').data == ['A']
assert tree.find('X') is None
class TestTrieNode(object):
"""Collection of tests for the TrieNode class."""
def test_add_child(self):
"""Verify we add children appropriately."""
node = trie.TrieNode('E', 'E is for Eat')
assert node.find_prefix('a') is None
added = node.add_child('a', 'a is for Apple')
assert node.find_prefix('a') is added
def test_add_child_overrides_previous_child(self):
"""Verify adding a child will replace the previous child."""
node = trie.TrieNode('E', 'E is for Eat', children={
'a': trie.TrieNode('a', 'a is for Apple')
})
previous = node.find_prefix('a')
assert previous is not None
added = node.add_child('a', 'a is for Ascertain')
assert node.find_prefix('a') is added
def test_find_prefix(self):
"""Verify we can find a child with the specified prefix."""
node = trie.TrieNode('E', 'E is for Eat', children={
'a': trie.TrieNode('a', 'a is for Apple')
})
child = node.find_prefix('a')
assert child is not None
assert child.prefix == 'a'
assert child.data == 'a is for Apple'
def test_find_prefix_returns_none_when_no_children_have_the_prefix(self):
"""Verify we receive None from find_prefix for missing children."""
node = trie.TrieNode('E', 'E is for Eat', children={
'a': trie.TrieNode('a', 'a is for Apple')
})
assert node.find_prefix('b') is None
def test_traverse_does_nothing_when_a_node_has_no_children(self):
"""Verify traversing a node with no children does nothing."""
node = trie.TrieNode('E', 'E is for Eat')
assert list(node.traverse()) == []
def test_traverse(self):
"""Verify traversal is depth-first and pre-order."""
root = trie.TrieNode(None, None)
node = root.add_child('A', 'A')
root.add_child('a', 'a')
node.add_child('B', 'B')
node = node.add_child('b', 'b')
node.add_child('C', 'C')
node.add_child('c', 'c')
# The sub-tree here should look something like
#
# <root>
# / \
# A a
# / |
# B b
# / \
# C c
#
# And the traversal should look like:
#
# A B b C c a
expected_order = ['A', 'B', 'b', 'C', 'c', 'a']
for expected, actual_node in zip(expected_order, root.traverse()):
assert actual_node.prefix == expected