forked from openkylin/flit
244 lines
7.3 KiB
Python
244 lines
7.3 KiB
Python
import errno
|
|
import pytest
|
|
import responses
|
|
|
|
from flit import validate as fv
|
|
|
|
def test_validate_entrypoints():
|
|
assert fv.validate_entrypoints(
|
|
{'console_scripts': {'flit': 'flit:main'}}) == []
|
|
assert fv.validate_entrypoints(
|
|
{'some.group': {'flit': 'flit.buildapi'}}) == []
|
|
|
|
res = fv.validate_entrypoints({'some.group': {'flit': 'a:b:c'}})
|
|
assert len(res) == 1
|
|
|
|
def test_validate_name():
|
|
def check(name):
|
|
return fv.validate_name({'name': name})
|
|
|
|
assert check('foo.bar_baz') == []
|
|
assert check('5minus6') == []
|
|
|
|
assert len(check('_foo')) == 1 # Must start with alphanumeric
|
|
assert len(check('foo.')) == 1 # Must end with alphanumeric
|
|
assert len(check('Bücher')) == 1 # ASCII only
|
|
|
|
def test_validate_requires_python():
|
|
assert fv.validate_requires_python({}) == [] # Not required
|
|
|
|
def check(spec):
|
|
return fv.validate_requires_python({'requires_python': spec})
|
|
|
|
assert check('>=3') == []
|
|
assert check('>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*') == []
|
|
|
|
assert len(check('3')) == 1
|
|
assert len(check('@12')) == 1
|
|
assert len(check('>=2.7; !=3.0.*')) == 1 # Comma separated, not semicolon
|
|
|
|
def test_validate_requires_dist():
|
|
assert fv.validate_requires_dist({}) == [] # Not required
|
|
|
|
def check(spec):
|
|
return fv.validate_requires_dist({'requires_dist': [spec]})
|
|
|
|
assert check('requests') == []
|
|
assert check('requests[extra-foo]') == []
|
|
assert check('requests (>=2.14)') == [] # parentheses allowed but not recommended
|
|
assert check('requests >=2.14') == []
|
|
assert check('pexpect; sys_platform == "win32"') == []
|
|
# Altogether now
|
|
assert check('requests[extra-foo] >=2.14; python_version < "3.0"') == []
|
|
|
|
# URL specifier
|
|
assert check('requests @ https://example.com/requests.tar.gz') == []
|
|
assert check(
|
|
'requests @ https://example.com/requests.tar.gz ; python_version < "3.8"'
|
|
) == []
|
|
|
|
# Problems
|
|
assert len(check('Bücher')) == 1
|
|
assert len(check('requests 2.14')) == 1
|
|
assert len(check('pexpect; sys.platform == "win32"')) == 1 # '.' -> '_'
|
|
assert len(check('requests >=2.14 @ https://example.com/requests.tar.gz')) == 1
|
|
# Several problems in one requirement
|
|
assert len(check('pexpect[_foo] =3; sys.platform == "win32"')) == 3
|
|
|
|
def test_validate_environment_marker():
|
|
vem = fv.validate_environment_marker
|
|
|
|
assert vem('python_version >= "3" and os_name == \'posix\'') == []
|
|
|
|
res = vem('python_version >= "3') # Unclosed string
|
|
assert len(res) == 1
|
|
assert res[0].startswith("Invalid string")
|
|
|
|
res = vem('python_verson >= "3"') # Misspelled name
|
|
assert len(res) == 1
|
|
assert res[0].startswith("Invalid variable")
|
|
|
|
res = vem("os_name is 'posix'") # No 'is' comparisons
|
|
assert len(res) == 1
|
|
assert res[0].startswith("Invalid expression")
|
|
|
|
res = vem("'2' < python_version < '4'") # No chained comparisons
|
|
assert len(res) == 1
|
|
assert res[0].startswith("Invalid expression")
|
|
|
|
assert len(vem('os.name == "linux\'')) == 2
|
|
|
|
def test_validate_url():
|
|
vurl = fv.validate_url
|
|
assert vurl("https://github.com/pypa/flit") == []
|
|
|
|
assert len(vurl("github.com/pypa/flit")) == 1
|
|
assert len(vurl("https://")) == 1
|
|
|
|
|
|
def test_validate_project_urls():
|
|
vpu = fv.validate_project_urls
|
|
|
|
def check(prurl):
|
|
return vpu({'project_urls': [prurl]})
|
|
assert vpu({}) == [] # Not required
|
|
assert check('Documentation, https://flit.readthedocs.io/') == []
|
|
|
|
# Missing https://
|
|
assert len(check('Documentation, flit.readthedocs.io')) == 1
|
|
# Double comma
|
|
assert len(check('A, B, flit.readthedocs.io')) == 1
|
|
# No name
|
|
assert len(check(', https://flit.readthedocs.io/')) == 1
|
|
# Name longer than 32 chars
|
|
assert len(check('Supercalifragilisticexpialidocious, https://flit.readthedocs.io/')) == 1
|
|
|
|
|
|
def test_read_classifiers_cached(monkeypatch, tmp_path):
|
|
|
|
def mock_get_cache_dir():
|
|
tmp_file = tmp_path / "classifiers.lst"
|
|
with tmp_file.open("w") as fh:
|
|
fh.write("A\nB\nC")
|
|
return tmp_path
|
|
|
|
monkeypatch.setattr(fv, "get_cache_dir", mock_get_cache_dir)
|
|
|
|
classifiers = fv._read_classifiers_cached()
|
|
|
|
assert classifiers == {'A', 'B', 'C'}
|
|
|
|
|
|
@responses.activate
|
|
def test_download_and_cache_classifiers(monkeypatch, tmp_path):
|
|
responses.add(
|
|
responses.GET,
|
|
'https://pypi.org/pypi?%3Aaction=list_classifiers',
|
|
body="A\nB\nC")
|
|
|
|
def mock_get_cache_dir():
|
|
return tmp_path
|
|
|
|
monkeypatch.setattr(fv, "get_cache_dir", mock_get_cache_dir)
|
|
|
|
classifiers = fv._download_and_cache_classifiers()
|
|
|
|
assert classifiers == {"A", "B", "C"}
|
|
|
|
|
|
def test_validate_classifiers_private(monkeypatch):
|
|
"""
|
|
Test that `Private :: Do Not Upload` considered a valid classifier.
|
|
This is a special case because it is not listed in a trove classifier
|
|
but it is a way to make sure that a private package is not get uploaded
|
|
on PyPI by accident.
|
|
|
|
Implementation on PyPI side:
|
|
https://github.com/pypa/warehouse/pull/5440
|
|
Issue about officially documenting the trick:
|
|
https://github.com/pypa/packaging.python.org/issues/643
|
|
"""
|
|
monkeypatch.setattr(fv, "_read_classifiers_cached", lambda: set())
|
|
|
|
actual = fv.validate_classifiers({'invalid'})
|
|
assert actual == ["Unrecognised classifier: 'invalid'"]
|
|
|
|
assert fv.validate_classifiers({'Private :: Do Not Upload'}) == []
|
|
|
|
|
|
@responses.activate
|
|
@pytest.mark.parametrize("error", [PermissionError, OSError(errno.EROFS, "")])
|
|
def test_download_and_cache_classifiers_with_unacessible_dir(monkeypatch, error):
|
|
responses.add(
|
|
responses.GET,
|
|
'https://pypi.org/pypi?%3Aaction=list_classifiers',
|
|
body="A\nB\nC")
|
|
|
|
class MockCacheDir:
|
|
def mkdir(self, parents):
|
|
raise error
|
|
def __truediv__(self, other):
|
|
raise error
|
|
|
|
monkeypatch.setattr(fv, "get_cache_dir", MockCacheDir)
|
|
|
|
classifiers = fv._download_and_cache_classifiers()
|
|
|
|
assert classifiers == {"A", "B", "C"}
|
|
|
|
|
|
def test_verify_classifiers_valid_classifiers():
|
|
classifiers = {"A"}
|
|
valid_classifiers = {"A", "B"}
|
|
|
|
problems = fv._verify_classifiers(classifiers, valid_classifiers)
|
|
|
|
assert problems == []
|
|
|
|
def test_verify_classifiers_invalid_classifiers():
|
|
classifiers = {"A", "B"}
|
|
valid_classifiers = {"A"}
|
|
|
|
problems = fv._verify_classifiers(classifiers, valid_classifiers)
|
|
|
|
assert problems == ["Unrecognised classifier: 'B'"]
|
|
|
|
def test_validate_readme_rst():
|
|
metadata = {
|
|
'description_content_type': 'text/x-rst',
|
|
'description': "Invalid ``rst'",
|
|
}
|
|
problems = fv.validate_readme_rst(metadata)
|
|
|
|
assert len(problems) == 2 # 1 message that rst is invalid + 1 with details
|
|
assert "valid rst" in problems[0]
|
|
|
|
# Markdown should be ignored
|
|
metadata = {
|
|
'description_content_type': 'text/markdown',
|
|
'description': "Invalid `rst'",
|
|
}
|
|
problems = fv.validate_readme_rst(metadata)
|
|
|
|
assert problems == []
|
|
|
|
RST_WITH_CODE = """
|
|
Code snippet:
|
|
|
|
.. code-block:: python
|
|
|
|
a = [i ** 2 for i in range(5)]
|
|
"""
|
|
|
|
def test_validate_readme_rst_code():
|
|
# Syntax highlighting shouldn't require pygments
|
|
metadata = {
|
|
'description_content_type': 'text/x-rst',
|
|
'description': RST_WITH_CODE,
|
|
}
|
|
problems = fv.validate_readme_rst(metadata)
|
|
for p in problems:
|
|
print(p)
|
|
|
|
assert problems == []
|