1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4
5import collections
6import itertools
7import os
8import platform
9import sys
10
11import pytest
12
13from packaging.markers import (
14    InvalidMarker,
15    Marker,
16    Node,
17    UndefinedComparison,
18    UndefinedEnvironmentName,
19    default_environment,
20    format_full_version,
21)
22
23VARIABLES = [
24    "extra",
25    "implementation_name",
26    "implementation_version",
27    "os_name",
28    "platform_machine",
29    "platform_release",
30    "platform_system",
31    "platform_version",
32    "python_full_version",
33    "python_version",
34    "platform_python_implementation",
35    "sys_platform",
36]
37
38PEP_345_VARIABLES = [
39    "os.name",
40    "sys.platform",
41    "platform.version",
42    "platform.machine",
43    "platform.python_implementation",
44]
45
46SETUPTOOLS_VARIABLES = ["python_implementation"]
47
48OPERATORS = ["===", "==", ">=", "<=", "!=", "~=", ">", "<", "in", "not in"]
49
50VALUES = [
51    "1.0",
52    "5.6a0",
53    "dog",
54    "freebsd",
55    "literally any string can go here",
56    "things @#4 dsfd (((",
57]
58
59
60class TestNode:
61    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
62    def test_accepts_value(self, value):
63        assert Node(value).value == value
64
65    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
66    def test_str(self, value):
67        assert str(Node(value)) == str(value)
68
69    @pytest.mark.parametrize("value", ["one", "two", None, 3, 5, []])
70    def test_repr(self, value):
71        assert repr(Node(value)) == f"<Node({str(value)!r})>"
72
73    def test_base_class(self):
74        with pytest.raises(NotImplementedError):
75            Node("cover all the code").serialize()
76
77
78class TestOperatorEvaluation:
79    def test_prefers_pep440(self):
80        assert Marker('"2.7.9" < "foo"').evaluate(dict(foo="2.7.10"))
81
82    def test_falls_back_to_python(self):
83        assert Marker('"b" > "a"').evaluate(dict(a="a"))
84
85    def test_fails_when_undefined(self):
86        with pytest.raises(UndefinedComparison):
87            Marker("'2.7.0' ~= os_name").evaluate()
88
89
90FakeVersionInfo = collections.namedtuple(
91    "FakeVersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]
92)
93
94
95class TestDefaultEnvironment:
96    def test_matches_expected(self):
97        environment = default_environment()
98
99        iver = "{0.major}.{0.minor}.{0.micro}".format(sys.implementation.version)
100        if sys.implementation.version.releaselevel != "final":
101            iver = "{0}{1[0]}{2}".format(
102                iver,
103                sys.implementation.version.releaselevel,
104                sys.implementation.version.serial,
105            )
106
107        assert environment == {
108            "implementation_name": sys.implementation.name,
109            "implementation_version": iver,
110            "os_name": os.name,
111            "platform_machine": platform.machine(),
112            "platform_release": platform.release(),
113            "platform_system": platform.system(),
114            "platform_version": platform.version(),
115            "python_full_version": platform.python_version(),
116            "platform_python_implementation": platform.python_implementation(),
117            "python_version": ".".join(platform.python_version_tuple()[:2]),
118            "sys_platform": sys.platform,
119        }
120
121    def test_multidigit_minor_version(self, monkeypatch):
122        version_info = (3, 10, 0, "final", 0)
123        monkeypatch.setattr(sys, "version_info", version_info, raising=False)
124
125        monkeypatch.setattr(platform, "python_version", lambda: "3.10.0", raising=False)
126        monkeypatch.setattr(
127            platform, "python_version_tuple", lambda: ("3", "10", "0"), raising=False
128        )
129
130        environment = default_environment()
131        assert environment["python_version"] == "3.10"
132
133    def tests_when_releaselevel_final(self):
134        v = FakeVersionInfo(3, 4, 2, "final", 0)
135        assert format_full_version(v) == "3.4.2"
136
137    def tests_when_releaselevel_not_final(self):
138        v = FakeVersionInfo(3, 4, 2, "beta", 4)
139        assert format_full_version(v) == "3.4.2b4"
140
141
142class TestMarker:
143    @pytest.mark.parametrize(
144        "marker_string",
145        [
146            "{} {} {!r}".format(*i)
147            for i in itertools.product(VARIABLES, OPERATORS, VALUES)
148        ]
149        + [
150            "{2!r} {1} {0}".format(*i)
151            for i in itertools.product(VARIABLES, OPERATORS, VALUES)
152        ],
153    )
154    def test_parses_valid(self, marker_string):
155        Marker(marker_string)
156
157    @pytest.mark.parametrize(
158        "marker_string",
159        [
160            "this_isnt_a_real_variable >= '1.0'",
161            "python_version",
162            "(python_version)",
163            "python_version >= 1.0 and (python_version)",
164        ],
165    )
166    def test_parses_invalid(self, marker_string):
167        with pytest.raises(InvalidMarker):
168            Marker(marker_string)
169
170    @pytest.mark.parametrize(
171        ("marker_string", "expected"),
172        [
173            # Test the different quoting rules
174            ("python_version == '2.7'", 'python_version == "2.7"'),
175            ('python_version == "2.7"', 'python_version == "2.7"'),
176            # Test and/or expressions
177            (
178                'python_version == "2.7" and os_name == "linux"',
179                'python_version == "2.7" and os_name == "linux"',
180            ),
181            (
182                'python_version == "2.7" or os_name == "linux"',
183                'python_version == "2.7" or os_name == "linux"',
184            ),
185            (
186                'python_version == "2.7" and os_name == "linux" or '
187                'sys_platform == "win32"',
188                'python_version == "2.7" and os_name == "linux" or '
189                'sys_platform == "win32"',
190            ),
191            # Test nested expressions and grouping with ()
192            ('(python_version == "2.7")', 'python_version == "2.7"'),
193            (
194                '(python_version == "2.7" and sys_platform == "win32")',
195                'python_version == "2.7" and sys_platform == "win32"',
196            ),
197            (
198                'python_version == "2.7" and (sys_platform == "win32" or '
199                'sys_platform == "linux")',
200                'python_version == "2.7" and (sys_platform == "win32" or '
201                'sys_platform == "linux")',
202            ),
203        ],
204    )
205    def test_str_and_repr(self, marker_string, expected):
206        m = Marker(marker_string)
207        assert str(m) == expected
208        assert repr(m) == f"<Marker({str(m)!r})>"
209
210    def test_extra_with_no_extra_in_environment(self):
211        # We can't evaluate an extra if no extra is passed into the environment
212        m = Marker("extra == 'security'")
213        with pytest.raises(UndefinedEnvironmentName):
214            m.evaluate()
215
216    @pytest.mark.parametrize(
217        ("marker_string", "environment", "expected"),
218        [
219            (f"os_name == '{os.name}'", None, True),
220            ("os_name == 'foo'", {"os_name": "foo"}, True),
221            ("os_name == 'foo'", {"os_name": "bar"}, False),
222            ("'2.7' in python_version", {"python_version": "2.7.5"}, True),
223            ("'2.7' not in python_version", {"python_version": "2.7"}, False),
224            (
225                "os_name == 'foo' and python_version ~= '2.7.0'",
226                {"os_name": "foo", "python_version": "2.7.6"},
227                True,
228            ),
229            (
230                "python_version ~= '2.7.0' and (os_name == 'foo' or "
231                "os_name == 'bar')",
232                {"os_name": "foo", "python_version": "2.7.4"},
233                True,
234            ),
235            (
236                "python_version ~= '2.7.0' and (os_name == 'foo' or "
237                "os_name == 'bar')",
238                {"os_name": "bar", "python_version": "2.7.4"},
239                True,
240            ),
241            (
242                "python_version ~= '2.7.0' and (os_name == 'foo' or "
243                "os_name == 'bar')",
244                {"os_name": "other", "python_version": "2.7.4"},
245                False,
246            ),
247            ("extra == 'security'", {"extra": "quux"}, False),
248            ("extra == 'security'", {"extra": "security"}, True),
249        ],
250    )
251    def test_evaluates(self, marker_string, environment, expected):
252        args = [] if environment is None else [environment]
253        assert Marker(marker_string).evaluate(*args) == expected
254
255    @pytest.mark.parametrize(
256        "marker_string",
257        [
258            "{} {} {!r}".format(*i)
259            for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
260        ]
261        + [
262            "{2!r} {1} {0}".format(*i)
263            for i in itertools.product(PEP_345_VARIABLES, OPERATORS, VALUES)
264        ],
265    )
266    def test_parses_pep345_valid(self, marker_string):
267        Marker(marker_string)
268
269    @pytest.mark.parametrize(
270        ("marker_string", "environment", "expected"),
271        [
272            (f"os.name == '{os.name}'", None, True),
273            ("sys.platform == 'win32'", {"sys_platform": "linux2"}, False),
274            ("platform.version in 'Ubuntu'", {"platform_version": "#39"}, False),
275            ("platform.machine=='x86_64'", {"platform_machine": "x86_64"}, True),
276            (
277                "platform.python_implementation=='Jython'",
278                {"platform_python_implementation": "CPython"},
279                False,
280            ),
281            (
282                "python_version == '2.5' and platform.python_implementation"
283                "!= 'Jython'",
284                {"python_version": "2.7"},
285                False,
286            ),
287        ],
288    )
289    def test_evaluate_pep345_markers(self, marker_string, environment, expected):
290        args = [] if environment is None else [environment]
291        assert Marker(marker_string).evaluate(*args) == expected
292
293    @pytest.mark.parametrize(
294        "marker_string",
295        [
296            "{} {} {!r}".format(*i)
297            for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
298        ]
299        + [
300            "{2!r} {1} {0}".format(*i)
301            for i in itertools.product(SETUPTOOLS_VARIABLES, OPERATORS, VALUES)
302        ],
303    )
304    def test_parses_setuptools_legacy_valid(self, marker_string):
305        Marker(marker_string)
306
307    def test_evaluate_setuptools_legacy_markers(self):
308        marker_string = "python_implementation=='Jython'"
309        args = [{"platform_python_implementation": "CPython"}]
310        assert Marker(marker_string).evaluate(*args) is False
311