From eeaf3926cb02198123eca7d110f38fd937b1e99d Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Thu, 13 Jan 2022 12:48:42 -0500 Subject: [PATCH 1/5] Add B019 check to find cache decorators on class methods --- README.rst | 5 +- bugbear.py | 34 ++++++++++++++ tests/b019.py | 103 ++++++++++++++++++++++++++++++++++++++++++ tests/test_bugbear.py | 19 ++++++++ 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 tests/b019.py diff --git a/README.rst b/README.rst index 09e87df..602261e 100644 --- a/README.rst +++ b/README.rst @@ -132,8 +132,11 @@ data available in ``ex``. **B018**: Found useless expression. Either assign it to a variable or remove it. -**B020**: Loop control variable overrides iterable it iterates +**B019**: Use of ``functools.lru_cache`` or ``functools.cache`` on class methods +can lead to memory leaks. The cache may retain instance references, preventing +garbage collection. +**B020**: Loop control variable overrides iterable it iterates Opinionated warnings ~~~~~~~~~~~~~~~~~~~~ diff --git a/bugbear.py b/bugbear.py index 177af11..1ffb164 100644 --- a/bugbear.py +++ b/bugbear.py @@ -350,6 +350,7 @@ def visit_FunctionDef(self, node): self.check_for_b902(node) self.check_for_b006(node) self.check_for_b018(node) + self.check_for_b019(node) self.generic_visit(node) def visit_ClassDef(self, node): @@ -378,6 +379,8 @@ def compose_call_path(self, node): if isinstance(node, ast.Attribute): yield from self.compose_call_path(node.value) yield node.attr + elif isinstance(node, ast.Call): + yield from self.compose_call_path(node.func) elif isinstance(node, ast.Name): yield node.id @@ -507,6 +510,24 @@ def check_for_b017(self, node): ): self.errors.append(B017(node.lineno, node.col_offset)) + def check_for_b019(self, node): + if ( + len(node.decorator_list) == 0 + or len(self.contexts) < 2 + or not isinstance(self.contexts[-2].node, ast.ClassDef) + ): + return + + resolved_decorators = { + ".".join(self.compose_call_path(decorator)) + for decorator in node.decorator_list + } + if resolved_decorators & {"classmethod", "staticmethod"}: + return + + if resolved_decorators & B019.caches: + self.errors.append(B019(node.lineno, node.col_offset)) + def check_for_b020(self, node): targets = NameFinder() targets.visit(node.target) @@ -886,6 +907,19 @@ def visit(self, node): "B018 Found useless expression. Either assign it to a variable or remove it." ) ) +B019 = Error( + message=( + "B019 Use of `functools.lru_cache` or `functools.cache` on class methods " + "can lead to memory leaks. The cache may retain instance references, " + "preventing garbage collection." + ) +) +B019.caches = { + "functools.cache", + "functools.lru_cache", + "cache", + "lru_cache", +} B020 = Error( message=( "B020 Found for loop that reassigns the iterable it is iterating " diff --git a/tests/b019.py b/tests/b019.py new file mode 100644 index 0000000..0052372 --- /dev/null +++ b/tests/b019.py @@ -0,0 +1,103 @@ +""" +Should emit: +B019 - on lines 74, 78, 82, 86, 90, 94, 98, 102 +""" +import functools +from functools import cache, cached_property, lru_cache + + +def some_other_cache(): + ... + + +class Foo: + def __init__(self, x): + self.x = x + + def compute_method(self, y): + ... + + @some_other_cache + def user_cached_method(self, y): + ... + + @classmethod + @functools.cache + def cached_classmethod(cls, y): + ... + + @classmethod + @cache + def other_cached_classmethod(cls, y): + ... + + @classmethod + @functools.lru_cache + def lru_cached_classmethod(cls, y): + ... + + @classmethod + @lru_cache + def other_lru_cached_classmethod(cls, y): + ... + + @staticmethod + @functools.cache + def cached_staticmethod(y): + ... + + @staticmethod + @cache + def other_cached_staticmethod(y): + ... + + @staticmethod + @functools.lru_cache + def lru_cached_staticmethod(y): + ... + + @staticmethod + @lru_cache + def other_lru_cached_staticmethod(y): + ... + + @functools.cached_property + def some_cached_property(self): + ... + + @cached_property + def some_other_cached_property(self): + ... + + # Remaining methods should emit B019 + @functools.cache + def cached_method(self, y): + ... + + @cache + def another_cached_method(self, y): + ... + + @functools.cache() + def called_cached_method(self, y): + ... + + @cache() + def another_called_cached_method(self, y): + ... + + @functools.lru_cache + def lru_cached_method(self, y): + ... + + @lru_cache + def another_lru_cached_method(self, y): + ... + + @functools.lru_cache() + def called_lru_cached_method(self, y): + ... + + @lru_cache() + def another_called_lru_cached_method(self, y): + ... diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 7e25386..ddb55f9 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -29,6 +29,7 @@ B016, B017, B018, + B019, B020, B901, B902, @@ -250,6 +251,24 @@ def test_b018_classes(self): expected.append(B018(33, 4)) self.assertEqual(errors, self.errors(*expected)) + def test_b019(self): + filename = Path(__file__).absolute().parent / "b019.py" + bbc = BugBearChecker(filename=str(filename)) + errors = list(bbc.run()) + self.assertEqual( + errors, + self.errors( + B019(74, 4), + B019(78, 4), + B019(82, 4), + B019(86, 4), + B019(90, 4), + B019(94, 4), + B019(98, 4), + B019(102, 4), + ), + ) + def test_b020(self): filename = Path(__file__).absolute().parent / "b020.py" bbc = BugBearChecker(filename=str(filename)) From 1f45c1380c8f799de2e8b4b63a19266c7feb574e Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Tue, 18 Jan 2022 21:29:11 -0700 Subject: [PATCH 2/5] B019: Change decorator resolution approach to retain lineno Starting in Python 3.8, the function node definition's `lineno` is changed to index its `def ...` line rather than the first line where its decorators start. This causes inconsistent line numbers across Python versions for the line reported by Flake8. We can use the decorator node location instead, which provides a consistent location, and makes sense because this hits on decorators. --- bugbear.py | 21 +++++++++++++++------ tests/b019.py | 2 +- tests/test_bugbear.py | 16 ++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/bugbear.py b/bugbear.py index 1ffb164..d500513 100644 --- a/bugbear.py +++ b/bugbear.py @@ -518,15 +518,24 @@ def check_for_b019(self, node): ): return - resolved_decorators = { + # Preserve decorator order so we can get the lineno from the decorator node rather than + # the function node (this location definition changes in Python 3.8) + resolved_decorators = ( ".".join(self.compose_call_path(decorator)) for decorator in node.decorator_list - } - if resolved_decorators & {"classmethod", "staticmethod"}: - return + ) + for idx, decorator in enumerate(resolved_decorators): + if decorator in {"classmethod", "staticmethod"}: + return - if resolved_decorators & B019.caches: - self.errors.append(B019(node.lineno, node.col_offset)) + if decorator in B019.caches: + self.errors.append( + B019( + node.decorator_list[idx].lineno, + node.decorator_list[idx].col_offset, + ) + ) + return def check_for_b020(self, node): targets = NameFinder() diff --git a/tests/b019.py b/tests/b019.py index 0052372..b074df1 100644 --- a/tests/b019.py +++ b/tests/b019.py @@ -1,6 +1,6 @@ """ Should emit: -B019 - on lines 74, 78, 82, 86, 90, 94, 98, 102 +B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101 """ import functools from functools import cache, cached_property, lru_cache diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index ddb55f9..8cb6c1d 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -258,14 +258,14 @@ def test_b019(self): self.assertEqual( errors, self.errors( - B019(74, 4), - B019(78, 4), - B019(82, 4), - B019(86, 4), - B019(90, 4), - B019(94, 4), - B019(98, 4), - B019(102, 4), + B019(73, 5), + B019(77, 5), + B019(81, 5), + B019(85, 5), + B019(89, 5), + B019(93, 5), + B019(97, 5), + B019(101, 5), ), ) From 5a2bdac159648009bfd6ddca33d5fb59f1e0aed4 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 2 Feb 2022 12:12:00 -0500 Subject: [PATCH 3/5] Update README verbiage * Prefer `extend-select` and `extend-ignore` for configuring opinionated warnings (`B9`) * Add deprecation note for Bugbear's internal handling of whether or not to emit `B9` codes * Add an example for `extend-immutable-call` specification --- README.rst | 69 +++++++++++++++++++++++++++++++++--------------------- bugbear.py | 2 +- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 602261e..695d057 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ flake8-bugbear .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black -A plugin for Flake8 finding likely bugs and design problems in your +A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle:: @@ -192,33 +192,39 @@ on the first line and urls or paths that are on their own line:: How to enable opinionated warnings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To enable these checks, specify a ``--select`` command-line option or -``select=`` option in your config file. As of Flake8 3.0, this option -is a whitelist (checks not listed are being implicitly disabled), so you -have to explicitly specify all checks you want enabled. For example:: - - [flake8] - max-line-length = 80 - max-complexity = 12 - ... - ignore = E501 - select = C,E,F,W,B,B901 - -Note that we're enabling the complexity checks, the PEP8 ``pycodestyle`` -errors and warnings, the pyflakes fatals and all default Bugbear checks. -Finally, we're also specifying B901 as a check that we want enabled. -Some checks might need other flake8 checks disabled - e.g. E501 must be -disabled for B950 to be hit. - -If you'd like all optional warnings to be enabled for you (future proof -your config!), say ``B9`` instead of ``B901``. You will need Flake8 3.2+ -for this feature. - -Note that ``pycodestyle`` also has a bunch of warnings that are disabled -by default. Those get enabled as soon as there is an ``ignore =`` line -in your configuration. I think this behavior is surprising so Bugbear's +To enable these checks, specify a ``--extend-select`` command-line option or +``extend-select=`` option in your config file (requires flake8 4.0+):: + + [flake8] + max-line-length = 80 + max-complexity = 12 + ... + extend-ignore = E501 + extend-select = B901 + +Some checks might need other flake8 checks disabled - e.g. E501 must be disabled for +B950 to be hit. + +If you'd like all optional warnings to be enabled for you (future proof your config!), +say ``B9`` instead of ``B901``. You will need flake8 3.2+ for this feature. + +For flake8 versions older than 4.0, you will need to use the ``--select`` command-line +option or ``select=`` option in your config file. As of flake8 3.0, this option is a +whitelist (checks not listed are implicitly disabled), so you have to explicitly specify +all checks you want enabled (e.g. ``select = C,E,F,W,B,B901``). + +The ``--extend-ignore`` command-line option and ``extend-ignore=`` config file option +require flake8 3.6+. For older flake8 versions, the ``--ignore`` and ``ignore=`` options +can be used. Using ``ignore`` will override all codes that are disabled by +default from all installed linters, so you will need to specify these codes in your +configuration to silence them. I think this behavior is surprising so Bugbear's opinionated warnings require explicit selection. +**Note:** Bugbear's enforcement of explicit opinionated warning selection is deprecated +and will be removed in a future release. It is recommended to use ``extend-ignore`` and +``extend-select`` in your flake8 configuration to avoid implicitly altering selected and +ignored codes. + Configuration ------------- @@ -226,7 +232,16 @@ The plugin currently has one setting: ``extend-immutable-calls``: Specify a list of additional immutable calls. This could be useful, when using other libraries that provide more immutable calls, -beside those already handled by ``flake8-bugbear``. Calls to these method will no longer raise a ``B008`` warning. +beside those already handled by ``flake8-bugbear``. Calls to these method will no longer +raise a ``B008`` warning. + +For example: + + [flake8] + max-line-length = 80 + max-complexity = 12 + ... + extend-immutable-calls = pathlib.Path, Path Tests / Lints --------------- diff --git a/bugbear.py b/bugbear.py index d500513..de4b467 100644 --- a/bugbear.py +++ b/bugbear.py @@ -127,7 +127,7 @@ def add_options(optmanager): help="Skip B008 test for additional immutable calls.", ) - @lru_cache() + @lru_cache() # noqa: B019 def should_warn(self, code): """Returns `True` if Bugbear should emit a particular warning. From d698e147ce88945f5021ecff7c0505a8ea1106b7 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 2 Feb 2022 14:49:25 -0500 Subject: [PATCH 4/5] B9: Make Bugbear aware of flake8's `extend-select` * The code for Bugbear's built-in filtering for opinionated warnings predates the addition of `extend-select` to flake8 (`v4.0`) so it's not part of the check for explicit specification of `B9` codes. * Switch from `Mock` to `argparse.Namespace` for specifying options to tests to match the incoming type from `flake8` and avoid mocking side-effects. --- README.rst | 2 +- bugbear.py | 12 +++++++++ tests/test_bugbear.py | 60 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 695d057..dd9776a 100644 --- a/README.rst +++ b/README.rst @@ -235,7 +235,7 @@ This could be useful, when using other libraries that provide more immutable cal beside those already handled by ``flake8-bugbear``. Calls to these method will no longer raise a ``B008`` warning. -For example: +For example:: [flake8] max-line-length = 80 diff --git a/bugbear.py b/bugbear.py index de4b467..87f1af8 100644 --- a/bugbear.py +++ b/bugbear.py @@ -138,12 +138,17 @@ def should_warn(self, code): As documented in the README, the user is expected to explicitly select the warnings. + + NOTE: This method is deprecated and will be removed in a future release. It is + recommended to use `extend-ignore` and `extend-select` in your flake8 + configuration to avoid implicitly altering selected and ignored codes. """ if code[:2] != "B9": # Normal warnings are safe for emission. return True if self.options is None: + # Without options configured, Bugbear will emit B9 but Flake8 will ignore LOG.info( "Options not provided to Bugbear, optional warning %s selected.", code ) @@ -153,6 +158,13 @@ def should_warn(self, code): if code[:i] in self.options.select: return True + # flake8 4.0+: Also check for codes in extend_select + if ( + hasattr(self.options, "extend_select") + and code[:i] in self.options.extend_select + ): + return True + LOG.info( "Optional warning %s not present in selected warnings: %r. Not " "firing it at all.", diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 8cb6c1d..4f7ceb2 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -4,8 +4,8 @@ import subprocess import sys import unittest +from argparse import Namespace from pathlib import Path -from unittest.mock import Mock from hypothesis import HealthCheck, given, settings from hypothesmith import from_grammar @@ -131,8 +131,9 @@ def test_b007(self): def test_b008_extended(self): filename = Path(__file__).absolute().parent / "b008_extended.py" - mock_options = Mock() - mock_options.extend_immutable_calls = ["fastapi.Depends", "fastapi.Query"] + mock_options = Namespace( + extend_immutable_calls=["fastapi.Depends", "fastapi.Query"] + ) bbc = BugBearChecker(filename=str(filename), options=mock_options) errors = list(bbc.run()) @@ -255,17 +256,20 @@ def test_b019(self): filename = Path(__file__).absolute().parent / "b019.py" bbc = BugBearChecker(filename=str(filename)) errors = list(bbc.run()) + + # Decorator location changes in the AST in 3.7 + col = 5 if sys.version_info > (3, 6) else 4 self.assertEqual( errors, self.errors( - B019(73, 5), - B019(77, 5), - B019(81, 5), - B019(85, 5), - B019(89, 5), - B019(93, 5), - B019(97, 5), - B019(101, 5), + B019(73, col), + B019(77, col), + B019(81, col), + B019(85, col), + B019(89, col), + B019(93, col), + B019(97, col), + B019(101, col), ), ) @@ -360,6 +364,40 @@ def test_b950(self): ), ) + def test_b9_select(self): + filename = Path(__file__).absolute().parent / "b950.py" + + mock_options = Namespace(select=["B950"]) + bbc = BugBearChecker(filename=str(filename), options=mock_options) + errors = list(bbc.run()) + self.assertEqual( + errors, + self.errors( + B950(7, 92, vars=(92, 79)), + B950(12, 103, vars=(103, 79)), + B950(14, 103, vars=(103, 79)), + B950(21, 97, vars=(97, 79)), + ), + ) + + def test_b9_extend_select(self): + filename = Path(__file__).absolute().parent / "b950.py" + + # select is always going to have a value, usually the default codes, but can + # also be empty + mock_options = Namespace(select=[], extend_select=["B950"]) + bbc = BugBearChecker(filename=str(filename), options=mock_options) + errors = list(bbc.run()) + self.assertEqual( + errors, + self.errors( + B950(7, 92, vars=(92, 79)), + B950(12, 103, vars=(103, 79)), + B950(14, 103, vars=(103, 79)), + B950(21, 97, vars=(97, 79)), + ), + ) + def test_selfclean_bugbear(self): filename = Path(__file__).absolute().parent.parent / "bugbear.py" proc = subprocess.run( From f8e2049a80968903534d31952af2cc064ca65457 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 2 Feb 2022 22:37:55 -0500 Subject: [PATCH 5/5] Style fixes from review Co-authored-by: Cooper Ry Lees --- README.rst | 45 ++++++++++++++++++++++--------------------- bugbear.py | 8 ++++---- tests/test_bugbear.py | 12 ++++++------ 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index dd9776a..97f683f 100644 --- a/README.rst +++ b/README.rst @@ -8,9 +8,9 @@ flake8-bugbear .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black -A plugin for flake8 finding likely bugs and design problems in your -program. Contains warnings that don't belong in pyflakes and -pycodestyle:: +A plugin for ``flake8`` finding likely bugs and design problems in your +program. Contains warnings that don't belong in ``pyflakes`` and +``pycodestyle``:: bug·bear (bŭg′bâr′) n. @@ -57,7 +57,7 @@ List of warnings **B001**: Do not use bare ``except:``, it also catches unexpected events like memory errors, interrupts, system exit, and so on. Prefer ``except Exception:``. If you're sure what you're doing, be explicit and write -``except BaseException:``. Disable E722 to avoid duplicate warnings. +``except BaseException:``. Disable ``E722`` to avoid duplicate warnings. **B002**: Python does not support the unary prefix increment. Writing ``++n`` is equivalent to ``+(+(n))``, which equals ``n``. You meant ``n @@ -170,7 +170,7 @@ See `the exception chaining tutorial `_ and highway patrol not stopping you if you drive < 5mph too fast. Disable -E501 to avoid duplicate warnings. Like E501, this error ignores long shebangs +``E501`` to avoid duplicate warnings. Like ``E501``, this error ignores long shebangs on the first line and urls or paths that are on their own line:: #! long shebang ignored @@ -192,38 +192,39 @@ on the first line and urls or paths that are on their own line:: How to enable opinionated warnings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To enable these checks, specify a ``--extend-select`` command-line option or -``extend-select=`` option in your config file (requires flake8 4.0+):: +To enable Bugbear's opinionated checks (``B9xx``), specify an ``--extend-select`` +command-line option or ``extend-select=`` option in your config file +(requires ``flake8 >=4.0``):: [flake8] max-line-length = 80 max-complexity = 12 ... extend-ignore = E501 - extend-select = B901 + extend-select = B950 -Some checks might need other flake8 checks disabled - e.g. E501 must be disabled for -B950 to be hit. +Some of Bugbear's checks require other ``flake8`` checks disabled - e.g. ``E501`` must +be disabled when enabling ``B950``. If you'd like all optional warnings to be enabled for you (future proof your config!), -say ``B9`` instead of ``B901``. You will need flake8 3.2+ for this feature. +say ``B9`` instead of ``B950``. You will need ``flake8 >=3.2`` for this feature. -For flake8 versions older than 4.0, you will need to use the ``--select`` command-line -option or ``select=`` option in your config file. As of flake8 3.0, this option is a -whitelist (checks not listed are implicitly disabled), so you have to explicitly specify -all checks you want enabled (e.g. ``select = C,E,F,W,B,B901``). +For ``flake8 <=4.0``, you will need to use the ``--select`` command-line option or +``select=`` option in your config file. For ``flake8 >=3.0``, this option is a whitelist +(checks not listed are implicitly disabled), so you have to explicitly specify all +checks you want enabled (e.g. ``select = C,E,F,W,B,B950``). The ``--extend-ignore`` command-line option and ``extend-ignore=`` config file option -require flake8 3.6+. For older flake8 versions, the ``--ignore`` and ``ignore=`` options -can be used. Using ``ignore`` will override all codes that are disabled by -default from all installed linters, so you will need to specify these codes in your -configuration to silence them. I think this behavior is surprising so Bugbear's +require ``flake8 >=3.6``. For older ``flake8`` versions, the ``--ignore`` and +``ignore=`` options can be used. Using ``ignore`` will override all codes that are +disabled by default from all installed linters, so you will need to specify these codes +in your configuration to silence them. I think this behavior is surprising so Bugbear's opinionated warnings require explicit selection. **Note:** Bugbear's enforcement of explicit opinionated warning selection is deprecated and will be removed in a future release. It is recommended to use ``extend-ignore`` and -``extend-select`` in your flake8 configuration to avoid implicitly altering selected and -ignored codes. +``extend-select`` in your ``flake8`` configuration to avoid implicitly altering selected +and/or ignored codes. Configuration ------------- diff --git a/bugbear.py b/bugbear.py index 87f1af8..426e418 100644 --- a/bugbear.py +++ b/bugbear.py @@ -148,7 +148,7 @@ def should_warn(self, code): return True if self.options is None: - # Without options configured, Bugbear will emit B9 but Flake8 will ignore + # Without options configured, Bugbear will emit B9 but flake8 will ignore LOG.info( "Options not provided to Bugbear, optional warning %s selected.", code ) @@ -158,7 +158,7 @@ def should_warn(self, code): if code[:i] in self.options.select: return True - # flake8 4.0+: Also check for codes in extend_select + # flake8 >=4.0: Also check for codes in extend_select if ( hasattr(self.options, "extend_select") and code[:i] in self.options.extend_select @@ -530,8 +530,8 @@ def check_for_b019(self, node): ): return - # Preserve decorator order so we can get the lineno from the decorator node rather than - # the function node (this location definition changes in Python 3.8) + # Preserve decorator order so we can get the lineno from the decorator node + # rather than the function node (this location definition changes in Python 3.8) resolved_decorators = ( ".".join(self.compose_call_path(decorator)) for decorator in node.decorator_list diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 4f7ceb2..2908149 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -257,17 +257,17 @@ def test_b019(self): bbc = BugBearChecker(filename=str(filename)) errors = list(bbc.run()) - # Decorator location changes in the AST in 3.7 - col = 5 if sys.version_info > (3, 6) else 4 + # AST Decorator column location for callable decorators changes in 3.7 + col = 5 if sys.version_info >= (3, 7) else 4 self.assertEqual( errors, self.errors( - B019(73, col), - B019(77, col), + B019(73, 5), + B019(77, 5), B019(81, col), B019(85, col), - B019(89, col), - B019(93, col), + B019(89, 5), + B019(93, 5), B019(97, col), B019(101, col), ),