Source code for coverage_pyver_pragma.grammar

#!/usr/bin/env python3
#
#  grammar.py
r"""
.. versionadded:: 0.2.0

As with ``coverage.py``, lines are marked with comments in the form::

	# pragma: no cover

With ``coverage_pyver_pragma``, the comment may be followed with an expression enclosed in parentheses::

	# pragma: no cover (<=py38 and !Windows)

Each expression consists of one or more tags
(:py:data:`VERSION_TAG`, :py:data:`PLATFORM_TAG` or :py:data:`IMPLEMENTATION_TAG`).
The tags can be joined with the keywords ``AND``, ``OR`` and ``NOT``, with the exclamation mark ``!`` implying ``NOT``.
Parentheses can be used to group sub expressions.
A series of tags without keywords between them are evaluated with ``AND``.

.. py:data:: VERSION_TAG

A ``VERSION_TAG`` comprises an optional comparator (one of ``<=``, ``<``, ``>=``, ``>``),
a version specifier in the form ``pyXX``, and an optional ``+`` to indicate ``>=``.


:bold-title:`Example:`

.. parsed-literal::

	<=py36
	>=py37
	<py38
	>py27
	py34+  # equivalent to >=py34


.. py:data::  PLATFORM_TAG

A ``PLATFORM_TAG`` comprises a single word which will be compared (ignoring case)
with the output of :func:`platform.system`.


:bold-title:`Example:`

.. parsed-literal::

	Windows
	Linux
	Darwin  # macOS
	Java

If the current platform cannot be determined all strings are treated as :py:obj:`True`.

.. raw:: latex

	\clearpage

.. py:data:: IMPLEMENTATION_TAG

An ``IMPLEMENTATION_TAG`` comprises a single word which will be compared (ignoring case)
with the output of :func:`platform.python_implementation`.


:bold-title:`Example:`

.. parsed-literal::

	CPython
	PyPy
	IronPython
	Jython


Examples
-----------

Ignore if the Python version is less than or equal to 3.7::

	# pragma: no cover (<=py37)

Ignore if running on Python 3.9::

	# pragma: no cover (py39)

Ignore if the Python version is greater than 3.6 and it's not running on PyPy::

	# pragma: no cover (>py36 and !PyPy)

Ignore if the Python version is less than 3.8 and it's running on Windows::

	# pragma: no cover (Windows and <py38)

Ignore when not running on macOS (Darwin)::

	# pragma: no cover (!Darwin)

Ignore when not running on CPython::

	# pragma: no cover (!CPython)

.. raw:: latex

	\clearpage

API Reference
----------------

.. automodulesumm:: coverage_pyver_pragma.grammar

.. autovariable:: GRAMMAR
	:no-value:

"""  # noqa: D400
#
#  Copyright © 2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import os
import platform
import sys

# 3rd party
import packaging.specifiers
from domdf_python_tools.doctools import prettify_docstrings
from domdf_python_tools.stringlist import DelimitedList
from pyparsing import (
		CaselessKeyword,
		CaselessLiteral,
		Combine,
		Group,
		Literal,
		OneOrMore,
		Optional,
		ParserElement,
		ParseResults,
		Word,
		infixNotation,
		nums,
		oneOf,
		opAssoc
		)

__all__ = (
		"ImplementationTag",
		"LogicalAND",
		"LogicalNOT",
		"LogicalOR",
		"LogicalOp",
		"PlatformTag",
		"VersionTag",
		"VERSION_TAG",
		"PLATFORM_TAG",
		"IMPLEMENTATION_TAG",
		"GRAMMAR",
		)

# This ensures coverage.py records the correct coverage for these modules
# when they are under test

# pylint: disable=loop-global-usage,dotted-import-in-loop
for module in [m for m in sys.modules if m.startswith("domdf_python_tools")]:  # pragma: no cover (macOS)
	if module in sys.modules:
		del sys.modules[module]
# pylint: enable=loop-global-usage,dotted-import-in-loop

PYTHON_VERSION = os.environ.get("COV_PYTHON_VERSION", '.'.join(platform.python_version_tuple()[:2]))
PLATFORM = os.environ.get("COV_PLATFORM", platform.system()).casefold()
PYTHON_IMPLEMENTATION = os.environ.get("COV_PYTHON_IMPLEMENTATION", platform.python_implementation()).casefold()


[docs]@prettify_docstrings class VersionTag(packaging.specifiers.SpecifierSet): """ Represents a ``VERSION_TAG`` in the expression grammar. A ``VERSION_TAG`` comprises an optional comparator (one of ``<=``, ``<``, ``>=``, ``>``), a version specifier in the form ``pyXX``, and an optional ``+`` to indicate ``>=``. :bold-title:`Examples:` .. parsed-literal:: <=py36 >=py37 <py38 >py27 py34+ :param tokens: """ def __init__(self, tokens: ParseResults): token_dict = dict(tokens["version"]) version = token_dict["version"][2:] if "plus" in token_dict and "comparator" in token_dict: raise SyntaxError("Cannot combine a comparator with the plus sign.") elif "plus" in token_dict: super().__init__(f">={version[0]}.{version[1:]}") elif "comparator" in token_dict: comparator = token_dict["comparator"] super().__init__(f"{comparator}{version[0]}.{version[1:]}") else: super().__init__(f"=={version[0]}.{version[1:]}")
[docs] def __repr__(self) -> str: # pragma: no cover return f"<{self.__class__.__name__}({str(self)!r})>"
def __bool__(self) -> bool: return PYTHON_VERSION in self
[docs]@prettify_docstrings class PlatformTag(str): """ Represents a ``PLATFORM_TAG`` in the expression grammar. A ``PLATFORM_TAG`` comprises a single word which will be compared (ignoring case) with the output of :func:`platform.system`. :bold-title:`Examples:` .. parsed-literal:: Windows Linux Darwin # macOS Java .. latex:clearpage:: If the current platform cannot be determined all strings are treated as :py:obj:`True`. :param tokens: """ __slots__ = () def __new__(cls, tokens: ParseResults) -> "PlatformTag": # noqa: D102 return super().__new__(cls, str(tokens["platform"]))
[docs] def __repr__(self) -> str: # pragma: no cover return f"<{self.__class__.__name__}({str(self)!r})>"
def __bool__(self) -> bool: if not PLATFORM: # pragma: no cover return True return PLATFORM == self.casefold()
[docs]@prettify_docstrings class ImplementationTag(str): """ Represents an ``IMPLEMENTATION_TAG`` in the expression grammar. An ``IMPLEMENTATION_TAG`` comprises a single word which will be compared (ignoring case) with the output of :func:`platform.python_implementation`. :bold-title:`Examples:` .. parsed-literal:: CPython PyPy IronPython Jython :param tokens: .. latex:vspace:: -10px """ __slots__ = () def __new__(cls, tokens: ParseResults) -> "ImplementationTag": # noqa: D102 return super().__new__(cls, str(tokens["implementation"]))
[docs] def __repr__(self) -> str: # pragma: no cover return f"<{self.__class__.__name__}({str(self)!r})>"
def __bool__(self) -> bool: return PYTHON_IMPLEMENTATION == self.casefold()
[docs]@prettify_docstrings class LogicalOp: """ Represents a logical operator (``AND``, ``OR``, and ``NOT / !``). :param tokens: """ def __init__(self, tokens: ParseResults): self.tokens = DelimitedList(tokens[0])
[docs] def __format__(self, format_spec: str) -> str: return self.tokens.__format__(format_spec)
[docs] def __getitem__(self, item): # noqa: MAN001,MAN002 return self.tokens[item]
[docs] def __str__(self) -> str: return f"[{self:, }]"
[docs] def __repr__(self) -> str: # pragma: no cover return f"<{self.__class__.__name__}({self})>"
[docs]@prettify_docstrings class LogicalAND(LogicalOp): """ Represents the ``AND`` logical operator. :param tokens: """ def __bool__(self) -> bool: return bool(self[0]) and bool(self[2])
[docs]@prettify_docstrings class LogicalOR(LogicalOp): """ Represents the ``OR`` logical operator. :param tokens: """ def __bool__(self) -> bool: return bool(self[0]) or bool(self[2])
[docs]@prettify_docstrings class LogicalNOT(LogicalOp): """ Represents the ``NOT / !`` logical operator. :param tokens: """ def __bool__(self) -> bool: return not bool(self[1])
# Logical operators AND = CaselessKeyword("and") OR = CaselessKeyword("or") NOT = CaselessKeyword("not") | Literal('!') # Grammar comprises (case insensitive): # Python versions (<=pyXXX, pyXXX+) PLUS = Literal('+').setResultsName("plus") LESS_THAN_EQUAL = "<=" LESS_THAN = '<' GREATER_THAN_EQUAL = ">=" GREATER_THAN = '>' OPS = [LESS_THAN, LESS_THAN_EQUAL, GREATER_THAN, GREATER_THAN_EQUAL] # pylint: disable=use-tuple-over-list COMPARATOR = Optional(oneOf(' '.join(OPS))).setResultsName("comparator") VERSION = Combine(CaselessLiteral("py") + Word(nums)).setResultsName("version") VERSION_TAG = Group(COMPARATOR + VERSION + Optional(PLUS)).setResultsName("version") VERSION_TAG.setParseAction(VersionTag) # Platforms (Windows, !Linux) # TODO: other platforms WINDOWS = CaselessLiteral("windows") LINUX = CaselessLiteral("linux") DARWIN = CaselessLiteral("darwin") JAVA = CaselessLiteral("java") PLATFORM_TAG = (WINDOWS | LINUX | DARWIN | JAVA).setResultsName("platform") PLATFORM_TAG.setParseAction(PlatformTag) # Implementations (CPython, !PyPy) # TODO: other python implementations CPYTHON = CaselessLiteral("cpython") PYPY = CaselessLiteral("pypy") JYTHON = CaselessLiteral("jython") IRONPYTHON = CaselessLiteral("ironpython") IMPLEMENTATION_TAG = (CPYTHON | PYPY | JYTHON | IRONPYTHON).setResultsName("implementation") IMPLEMENTATION_TAG.setParseAction(ImplementationTag) ELEMENTS = VERSION_TAG | PLATFORM_TAG | IMPLEMENTATION_TAG GRAMMAR: ParserElement = OneOrMore( infixNotation( ELEMENTS, [ (NOT, 1, opAssoc.RIGHT, LogicalNOT), (AND, 2, opAssoc.LEFT, LogicalAND), (OR, 2, opAssoc.LEFT, LogicalOR), ] ) ) """ The :mod:`coverage_pyver_pragma` expression grammar. This can be used to parse an expression outside of the coverage context. """