|
2 | 2 | #
|
3 | 3 | # __init__.py
|
4 | 4 | """
|
5 |
| -Plugin for `Coverage.py <https://coverage.readthedocs.io/en/coverage-5.3/>`_ |
6 |
| -to selectively ignore branches depending on the Python version. |
7 |
| -""" # noqa: D400 |
| 5 | +Plugin for Coverage.py to selectively ignore branches depending on the Python version. |
| 6 | +""" |
8 | 7 | #
|
9 | 8 | # Copyright © 2020-2021 Dominic Davis-Foster <[email protected]>
|
10 | 9 | #
|
|
28 | 27 | #
|
29 | 28 |
|
30 | 29 | # stdlib
|
31 |
| -import platform |
32 | 30 | import re
|
33 |
| -import sys |
34 |
| -from typing import TYPE_CHECKING, Any, List, NamedTuple, Pattern, Tuple, Union |
| 31 | +from contextlib import suppress |
35 | 32 |
|
36 | 33 | # 3rd party
|
37 |
| -import coverage # type: ignore |
38 |
| - |
39 |
| -if TYPE_CHECKING: |
| 34 | +import coverage.python # type: ignore |
| 35 | +import pyparsing # type: ignore |
| 36 | +from coverage.config import DEFAULT_EXCLUDE # type: ignore |
| 37 | +from coverage.misc import join_regex # type: ignore |
40 | 38 |
|
41 |
| - # stdlib |
42 |
| - from sys import _version_info as VersionInfo # pragma: no cover (typing only) |
| 39 | +# this package |
| 40 | +from coverage_pyver_pragma.grammar import GRAMMAR |
43 | 41 |
|
44 | 42 | __author__: str = "Dominic Davis-Foster"
|
45 | 43 | __copyright__: str = "2020-2021 Dominic Davis-Foster"
|
46 | 44 | __license__: str = "MIT License"
|
47 |
| -__version__: str = "0.0.6" |
| 45 | +__version__: str = "0.2.0" |
48 | 46 | __email__: str = "[email protected]"
|
49 | 47 |
|
50 |
| -__all__ = [ |
51 |
| - "regex_main", |
52 |
| - "Version", |
53 |
| - "make_regexes", |
54 |
| - "PyVerPragmaPlugin", |
55 |
| - "make_not_exclude_regexs", |
56 |
| - "coverage_init", |
57 |
| - ] |
58 |
| - |
59 |
| -regex_main: str = re.compile(r"(?i)#\s*pragma[:\s]?\s*no\s*cover").pattern |
60 |
| - |
| 48 | +__all__ = ["DSL_EXCLUDE", "evaluate_exclude"] |
61 | 49 |
|
62 |
| -class Version(NamedTuple): |
63 |
| - """ |
64 |
| - :class:`~typing.NamedTuple` with the same elements as :py:data:`sys.version_info`. |
65 |
| - """ |
| 50 | +DSL_EXCLUDE = re.compile(r'.*#\s*(?:pragma|PRAGMA)[:\s]?\s*(?:no|NO)\s*(?:cover|COVER)\s*\((.*)\)') |
| 51 | +""" |
| 52 | +Compiled regular expression to match comments in the for ``# pragma: no cover (XXX)``, |
| 53 | +where ``XXX`` is an expression to be evaluated to determine whether the line should be excluded from coverage. |
66 | 54 |
|
67 |
| - major: int |
68 |
| - minor: int |
69 |
| - micro: int = 0 |
70 |
| - releaselevel: str = '' |
71 |
| - serial: str = '' |
| 55 | +.. versionadded:: 0.2.0 |
| 56 | +""" |
72 | 57 |
|
73 | 58 |
|
74 |
| -def make_regexes( |
75 |
| - version_tuple: Union["Version", "VersionInfo", Tuple[int, ...]], |
76 |
| - current_platform: str, |
77 |
| - current_implementation: str, |
78 |
| - ) -> List[Pattern]: |
| 59 | +def evaluate_exclude(expression: str) -> bool: |
79 | 60 | """
|
80 |
| - Generates a list of regular expressions to match all valid ignores for the given Python version. |
| 61 | + Evaluate the given expression to determine whether the line should be excluded from coverage. |
81 | 62 |
|
82 |
| - :param version_tuple: The Python version. |
83 |
| - :type version_tuple: :class:`~typing.NamedTuple` with the attributes ``major`` and ``minor`` |
84 |
| - :param current_platform: |
85 |
| - :param current_implementation: |
| 63 | + .. versionadded:: 0.2.0 |
86 | 64 |
|
87 |
| - :return: List of regular expressions. |
| 65 | + :param expression: |
88 | 66 | """
|
89 | 67 |
|
90 |
| - version_tuple = Version(*version_tuple) |
91 |
| - |
92 |
| - if version_tuple.major == 3: |
93 |
| - # Python 3.X |
94 |
| - |
95 |
| - greater_than_versions = [str(x) for x in range(0, version_tuple.minor)] |
96 |
| - greater_equal_versions = [*greater_than_versions, str(version_tuple.minor)] |
97 |
| - less_than_versions = [str(x) for x in range(version_tuple.minor + 1, 10)] |
98 |
| - # Current max Python version is 3.9 |
99 |
| - less_equal_versions = [str(version_tuple.minor), *less_than_versions] # pragma: no cover (!Linux) |
100 |
| - exact_versions = [str(version_tuple.minor)] |
101 |
| - |
102 |
| - wrong_platforms_string = fr"(?!.*!{current_platform})" # (?!.*Windows)(?!.*Darwin) |
103 |
| - wrong_implementations_string = fr"(?!.*!{current_implementation})" # (?!.*Windows)(?!.*Darwin) |
104 |
| - # correct_platforms_string = r"(?=\s*(Linux)?)" |
105 |
| - |
106 |
| - # Add regular expressions for relevant python versions |
107 |
| - # We do it with re.compile to get the syntax highlighting in PyCharm |
108 |
| - excludes = [ |
109 |
| - re.compile( |
110 |
| - fr"{regex_main}\s*\((?=\s*<py3({'|'.join(less_than_versions)})){wrong_platforms_string}{wrong_implementations_string}.*\)" |
111 |
| - ), |
112 |
| - re.compile( |
113 |
| - fr"{regex_main}\s*\((?=\s*<=py3({'|'.join(less_equal_versions)})){wrong_platforms_string}{wrong_implementations_string}.*\)" |
114 |
| - ), |
115 |
| - re.compile( |
116 |
| - fr"{regex_main}\s*\((?=\s*>py3({'|'.join(greater_than_versions)})){wrong_platforms_string}{wrong_implementations_string}.*\)" |
117 |
| - ), |
118 |
| - re.compile( |
119 |
| - fr"{regex_main}\s*\((?=\s*py3({'|'.join(greater_equal_versions)})\+){wrong_platforms_string}{wrong_implementations_string}.*\)" |
120 |
| - ), |
121 |
| - re.compile( |
122 |
| - fr"{regex_main}\s*\((?=\s*>=py3({'|'.join(greater_equal_versions)})){wrong_platforms_string}{wrong_implementations_string}.*\)" |
123 |
| - ), |
124 |
| - re.compile( |
125 |
| - fr"{regex_main}\s*\((?=\s*py3({'|'.join(exact_versions)})){wrong_platforms_string}{wrong_implementations_string}.*\)" |
126 |
| - ), |
127 |
| - re.compile( |
128 |
| - fr"{regex_main}\s*\((?!.*py[0-9]+){wrong_platforms_string}{wrong_implementations_string}.*\)" |
129 |
| - ), |
130 |
| - ] |
131 |
| - |
132 |
| - # print(excludes) |
133 |
| - return excludes |
134 |
| - |
135 |
| - else: |
136 |
| - raise ValueError("Unknown Python version.") |
137 |
| - |
138 |
| - |
139 |
| -class PyVerPragmaPlugin(coverage.CoveragePlugin): |
140 |
| - """ |
141 |
| - Plugin for `Coverage.py <https://coverage.readthedocs.io/en/coverage-5.3/>`_ |
142 |
| - to selectively ignore branches depending on the Python version. |
143 |
| - """ # noqa: D400 |
144 |
| - |
145 |
| - def configure(self, config: Any) -> None: |
146 |
| - """ |
147 |
| - Configure the plugin. |
148 |
| -
|
149 |
| - :param config: |
150 |
| - """ |
151 |
| - |
152 |
| - # Coverage.py gives either a Coverage() object, or a CoverageConfig() object. |
153 |
| - if isinstance(config, coverage.Coverage): |
154 |
| - config = config.config # pragma: no cover |
| 68 | + return all(list(GRAMMAR.parseString(expression.lower(), parseAll=True))) |
155 | 69 |
|
156 |
| - # Remove standard "pragma: no cover" regex |
157 |
| - if regex_main in config.exclude_list: |
158 |
| - config.exclude_list.remove(regex_main) |
159 | 70 |
|
160 |
| - if "pragma: no cover" in config.exclude_list: |
161 |
| - config.exclude_list.remove("pragma: no cover") |
| 71 | +class PythonParser(coverage.python.PythonParser): # noqa: D102 |
162 | 72 |
|
163 |
| - excludes = make_regexes(sys.version_info, platform.system(), platform.python_implementation()) |
164 |
| - for exc_pattern in excludes: |
165 |
| - config.exclude_list.append(exc_pattern.pattern) |
| 73 | + def lines_matching(self, *regexes): # noqa: D102 |
166 | 74 |
|
167 |
| - # Reinstate the general regex, but making sure it isn't followed by a left bracket. |
168 |
| - config.exclude_list += [ |
169 |
| - p.pattern for p in make_not_exclude_regexs(platform.system(), platform.python_implementation()) |
170 |
| - ] |
| 75 | + combined = join_regex([*regexes, *DEFAULT_EXCLUDE]) |
171 | 76 |
|
172 |
| - # TODO: Python 4.X |
| 77 | + regex_c = re.compile(combined) |
| 78 | + matches = set() |
173 | 79 |
|
| 80 | + for idx, ltext in enumerate(self.lines, start=1): |
174 | 81 |
|
175 |
| -def make_not_exclude_regexs( |
176 |
| - current_platform: str, |
177 |
| - current_implementation: str, |
178 |
| - ) -> List[Pattern]: |
179 |
| - """ |
180 |
| - Generates a list of regular expressions for lines that should not be excluded. |
| 82 | + dsl_m = DSL_EXCLUDE.match(ltext) |
181 | 83 |
|
182 |
| - :param current_platform: |
183 |
| - :param current_implementation: |
184 |
| - """ |
| 84 | + # Check if it matches the DSL regex: |
| 85 | + if dsl_m: |
| 86 | + exclude_source = dsl_m.group(1) |
185 | 87 |
|
186 |
| - return [ |
187 |
| - re.compile( |
188 |
| - fr"{regex_main} (?!\(.*(.{{0,2}}py3\d(\+)?|!{current_platform}|!{current_implementation}).*\)).*$" |
189 |
| - ), |
190 |
| - re.compile(fr"{regex_main}$"), |
191 |
| - ] |
| 88 | + with suppress(pyparsing.ParseBaseException): |
| 89 | + if evaluate_exclude(exclude_source): |
| 90 | + matches.add(idx) |
| 91 | + continue |
192 | 92 |
|
| 93 | + if regex_c.search(ltext): |
| 94 | + matches.add(idx) |
193 | 95 |
|
194 |
| -def coverage_init(reg, options): |
195 |
| - """ |
196 |
| - Initialise the plugin. |
| 96 | + return matches |
197 | 97 |
|
198 |
| - :param reg: |
199 |
| - :param options: |
200 |
| - """ |
201 | 98 |
|
202 |
| - reg.add_configurer(PyVerPragmaPlugin()) # pragma: no cover |
| 99 | +def coverage_init(*args, **kwargs): |
| 100 | + coverage.python.PythonParser.lines_matching = PythonParser.lines_matching |
0 commit comments