File: test_config.py

package info (click to toggle)
flit 3.12.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,504 kB
  • sloc: python: 6,691; makefile: 175; sh: 27
file content (406 lines) | stat: -rw-r--r-- 15,971 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
import logging
import re
import sys
from pathlib import Path
import pytest

from flit_core import config

samples_dir = Path(__file__).parent / 'samples'

def test_flatten_entrypoints():
    r = config.flatten_entrypoints({'a': {'b': {'c': 'd'}, 'e': {'f': {'g': 'h'}}, 'i': 'j'}})
    assert r == {'a': {'i': 'j'}, 'a.b': {'c': 'd'}, 'a.e.f': {'g': 'h'}}

def test_load_toml():
    inf = config.read_flit_config(samples_dir / 'module1-pkg.toml')
    assert inf.module == 'module1'
    assert inf.metadata['home_page'] == 'http://github.com/sirrobin/module1'

def test_load_toml_ns():
    inf = config.read_flit_config(samples_dir / 'ns1-pkg' / 'pyproject.toml')
    assert inf.module == 'ns1.pkg'
    assert inf.metadata['home_page'] == 'http://github.com/sirrobin/module1'

def test_load_normalization():
    inf = config.read_flit_config(samples_dir / 'normalization' / 'pyproject.toml')
    assert inf.module == 'my_python_module'
    assert inf.metadata['name'] == 'my-python-module'

def test_load_pep621():
    inf = config.read_flit_config(samples_dir / 'pep621' / 'pyproject.toml')
    assert inf.module == 'module1a'
    assert inf.metadata['name'] == 'module1'
    assert inf.metadata['description_content_type'] == 'text/x-rst'
    # Remove all whitespace from requirements so we don't check exact format:
    assert {r.replace(' ', '') for r in inf.metadata['requires_dist']} == {
        'docutils',
        'requests>=2.18',
        'pytest;extra=="test"',  # from [project.optional-dependencies]
        'mock;extra=="test"and(python_version<\'3.6\')',
    }
    assert inf.metadata['author_email'] == "Sir Röbin <robin@camelot.uk>"
    assert inf.entrypoints['flit_test_example']['foo'] == 'module1:main'
    assert set(inf.dynamic_metadata) == {'version', 'description'}

def test_load_pep621_nodynamic():
    inf = config.read_flit_config(samples_dir / 'pep621_nodynamic' / 'pyproject.toml')
    assert inf.module == 'module1'
    assert inf.metadata['name'] == 'module1'
    assert inf.metadata['version'] == '0.3'
    assert inf.metadata['summary'] == 'Statically specified description'
    assert set(inf.dynamic_metadata) == set()

    # Filling reqs_by_extra when dependencies were specified but no optional
    # dependencies was a bug.
    assert inf.reqs_by_extra == {'.none':  ['requests >= 2.18', 'docutils']}

def test_misspelled_key():
    with pytest.raises(config.ConfigError) as e_info:
        config.read_flit_config(samples_dir / 'misspelled-key.toml')

    assert 'description-file' in str(e_info.value)

def test_description_file():
    info = config.read_flit_config(samples_dir / 'package1.toml')
    assert info.metadata['description'] == \
        "Sample description for test.\n"
    assert info.metadata['description_content_type'] == 'text/x-rst'

def test_missing_description_file():
    with pytest.raises(config.ConfigError, match=r"Description file .* does not exist"):
        config.read_flit_config(samples_dir / 'missing-description-file.toml')

def test_bad_description_extension(caplog):
    info = config.read_flit_config(samples_dir / 'bad-description-ext.toml')
    assert info.metadata['description_content_type'] is None
    assert any((r.levelno == logging.WARN and "Unknown extension" in r.msg)
                for r in caplog.records)

def test_extras():
    info = config.read_flit_config(samples_dir / 'extras.toml')
    requires_dist = set(info.metadata['requires_dist'])
    assert requires_dist == {
        'toml',
        'pytest ; extra == "test"',
        'requests ; extra == "cus-tom"',
    }
    assert set(info.metadata['provides_extra']) == {'test', 'cus-tom'}

def test_extras_newstyle():
    # As above, but with new-style [project] table
    info = config.read_flit_config(samples_dir / 'extras-newstyle.toml')
    requires_dist = set(info.metadata['requires_dist'])
    assert requires_dist == {
        'toml',
        'pytest ; extra == "test"',
        'requests ; extra == "cus-tom"',
    }
    assert set(info.metadata['provides_extra']) == {'test', 'cus-tom'}

def test_extras_dev_conflict():
    with pytest.raises(config.ConfigError, match=r'dev-requires'):
        config.read_flit_config(samples_dir / 'extras-dev-conflict.toml')

def test_extras_dev_warning(caplog):
    info = config.read_flit_config(samples_dir / 'requires-dev.toml')
    assert '"dev-requires = ..." is obsolete' in caplog.text
    assert set(info.metadata['requires_dist']) == {'apackage ; extra == "dev"'}

def test_requires_extra_env_marker():
    info = config.read_flit_config(samples_dir / 'requires-extra-envmark.toml')
    assert info.metadata['requires_dist'][0].startswith('pathlib2 ;')

@pytest.mark.parametrize(('erroneous', 'match'), [
    ({'requires-extra': None}, r'Expected a dict for requires-extra field'),
    ({'requires-extra': dict(dev=None)}, r'Expected a dict of lists for requires-extra field'),
    ({'requires-extra': dict(dev=[1])}, r'Expected a string list for requires-extra'),
])
def test_faulty_requires_extra(erroneous, match):
    metadata = {'module': 'mymod', 'author': '', 'author-email': ''}
    with pytest.raises(config.ConfigError, match=match):
        config._prep_metadata(dict(metadata, **erroneous), None)

@pytest.mark.parametrize(('path', 'err_match'), [
    ('../bar', 'out of the directory'),
    ('foo/../../bar', 'out of the directory'),
    ('/home', 'absolute path'),
    ('foo:bar', 'bad character'),
])
def test_bad_include_paths(path, err_match):
    toml_cfg = {'tool': {'flit': {
        'metadata': {'module': 'xyz', 'author': 'nobody'},
        'sdist': {'include': [path]}
    }}}

    with pytest.raises(config.ConfigError, match=err_match):
        config.prep_toml_config(toml_cfg, None)

@pytest.mark.parametrize(('proj_bad', 'err_match'), [
    ({'version': 1}, r'\bstr\b'),
    ({'license': {'fromage': 2}}, '[Uu]nrecognised'),
    ({'license': {'file': 'LICENSE', 'text': 'xyz'}}, 'both'),
    (
        {'license': {'file': '/LICENSE'}},
        re.escape("License file path (/LICENSE) cannot be an absolute path"),
    ),
    (
        {'license': {'file': '../LICENSE'}},
        re.escape("License file path (../LICENSE) cannot contain '..'"),
    ),
    ({'license': {}}, 'required'),
    ({'license': 1}, "license field should be <class 'str'> or <class 'dict'>, not <class 'int'>"),
    # ({'license': "MIT License"}, "Invalid license expression: 'MIT License'"),  # TODO
    (
        {'license': 'MIT', 'classifiers': ['License :: OSI Approved :: MIT License']},
        "License classifiers are deprecated in favor of the license expression",
    ),
    ({'license-files': 1}, r"\blist\b"),
    ({'license-files': ["/LICENSE"]}, r"'/LICENSE'.+must not start with '/'"),
    ({'license-files': ["../LICENSE"]}, r"'../LICENSE'.+must not contain '..'"),
    ({'license-files': ["NOT_FOUND"]}, r"No files found.+'NOT_FOUND'"),
    ({'license-files': ["(LICENSE | LICENCE)"]}, "Pattern contains invalid characters"),
    pytest.param(
        {'license-files': ["**LICENSE"]}, r"'\*\*LICENSE'.+Invalid pattern",
        marks=[pytest.mark.skipif(
            sys.version_info >= (3, 13), reason="Pattern is valid for 3.13+"
        )]
    ),
    pytest.param(
        {'license-files': ["./"]}, r"'./'.+Unacceptable pattern",
        marks=[pytest.mark.skipif(
            sys.version_info < (3, 13), reason="Pattern started to raise ValueError in 3.13"
        )]
    ),
    (
        {'license': {'file': 'LICENSE'}, 'license-files': ["LICENSE"]},
        "license-files cannot be used with a license table",
    ),
    ({'keywords': 'foo'}, 'list'),
    ({'keywords': ['foo', 7]}, 'strings'),
    ({'entry-points': {'foo': 'module1:main'}}, 'entry-point.*tables'),
    ({'entry-points': {'group': {'foo': 7}}}, 'entry-point.*string'),
    ({'entry-points': {'gui_scripts': {'foo': 'a:b'}}}, r'\[project\.gui-scripts\]'),
    ({'scripts': {'foo': 7}}, 'scripts.*string'),
    ({'gui-scripts': {'foo': 7}}, 'gui-scripts.*string'),
    ({'optional-dependencies': {'test': 'requests'}}, 'list.*optional-dep'),
    ({'optional-dependencies': {'test': [7]}}, 'string.*optional-dep'),
    ({'dynamic': ['classifiers']}, 'dynamic'),
    ({'dynamic': ['version']}, r'dynamic.*\[project\]'),
    ({'authors': ['thomas']}, r'author.*\bdict'),
    ({'maintainers': [{'title': 'Dr'}]}, r'maintainer.*title'),
    ({'name': 'mödule1'}, r'not valid'),
    ({'name': 'module1_'}, r'not valid'),
    ({'optional-dependencies': {'x_': []}}, r'not valid'),
    ({'optional-dependencies': {'x_a': [], 'X--a': []}}, r'clash'),
])
def test_bad_pep621_info(proj_bad, err_match):
    proj = {'name': 'module1', 'version': '1.0', 'description': 'x'}
    proj.update(proj_bad)
    with pytest.raises(config.ConfigError, match=err_match):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')

@pytest.mark.parametrize(('readme', 'err_match'), [
    ({'file': 'README.rst'}, 'required'),
    ({'file': 'README.rst', 'content-type': 'text/x-python'}, 'content-type'),
    ('/opt/README.rst', 'relative'),
    ({'file': 'README.rst', 'text': '', 'content-type': 'text/x-rst'}, 'both'),
    ({'content-type': 'text/x-rst'}, 'required'),
    ({'file': 'README.rst', 'content-type': 'text/x-rst', 'a': 'b'}, '[Uu]nrecognised'),
    (5, r'readme.*string'),
])
def test_bad_pep621_readme(readme, err_match):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x', 'readme': readme
    }
    with pytest.raises(config.ConfigError, match=err_match):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize(('value', 'license_expression'), [
    # Accept and normalize valid SPDX expressions for 'license = ...'
    ("mit",  "MIT"),
    ("apache-2.0", "Apache-2.0"),
    ("APACHE-2.0+", "Apache-2.0+"),
    ("mit AND (apache-2.0 OR bsd-2-clause)", "MIT AND (Apache-2.0 OR BSD-2-Clause)"),
    ("(mit)", "(MIT)"),
    ("MIT OR Apache-2.0", "MIT OR Apache-2.0"),
    ("MIT AND Apache-2.0", "MIT AND Apache-2.0"),
    ("MIT AND Apache-2.0+ OR 0BSD", "MIT AND Apache-2.0+ OR 0BSD"),
    ("MIT AND (Apache-2.0+ OR (0BSD))", "MIT AND (Apache-2.0+ OR (0BSD))"),
    ("MIT OR(mit)", "MIT OR (MIT)"),
    ("(mit)AND mit", "(MIT) AND MIT"),
    ("MIT OR (MIT OR ( MIT )) AND ((MIT) AND MIT) OR MIT", "MIT OR (MIT OR (MIT)) AND ((MIT) AND MIT) OR MIT"),
    ("LICENSEREF-Public-Domain OR cc0-1.0 OR unlicense", "LicenseRef-Public-Domain OR CC0-1.0 OR Unlicense"),
    ("mit  AND  ( apache-2.0+  OR  mpl-2.0+ )", "MIT AND (Apache-2.0+ OR MPL-2.0+)"),
    # LicenseRef expressions: only the LicenseRef is normalised
    ("LiceNseref-Public-DoMain", "LicenseRef-Public-DoMain"),
])
def test_license_expr(value, license_expression):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x', 'license': value
    }
    info = config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')
    assert 'license' not in info.metadata
    assert info.metadata['license_expression'] == license_expression

@pytest.mark.parametrize('invalid_expr', [
    "LicenseRef-foo_bar",
    "LicenseRef-foo~bar",
    "LicenseRef-foo:bar",
    "LicenseRef-foo[bar]",
    "LicenseRef-foo-bar+",
])
def test_license_expr_error_licenseref(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="can only contain"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize('invalid_expr', [
    # Not a real licence
    "BSD-33-Clause",
    "MIT OR BSD-33-Clause",
    "MIT OR (MIT AND BSD-33-Clause)",
])
def test_license_expr_error_not_recognised(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="recognised"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize('invalid_expr', [
    # No operator
    "MIT MIT",
    "MIT OR (MIT MIT)",
    # Only operator
    "AND",
    "OR",
    "AND AND AND",
    "OR OR OR",
    "OR AND OR",
    "AND OR OR AND OR OR AND",
    # Too many operators
    "MIT AND AND MIT",
    "MIT OR OR OR MIT",
    "MIT AND OR MIT",
    # Mixed case operator
    "MIT aND MIT",
    "MIT oR MIT",
    "MIT AND MIT oR MIT",
    # Missing operand
    "MIT AND",
    "AND MIT",
    "MIT OR",
    "OR MIT",
    "MIT (AND MIT)",
    "(MIT OR) MIT",
    # Unbalanced brackets
    ")(",
    "(",
    ")",
    "MIT OR ()",
    ") AND MIT",
    "MIT OR (",
    "MIT OR (MIT))",
    # Only brackets
    "()",
    "()()",
    "()(())",
    "(  )",
    "  (  )",
    "(  )  ",
    "  (  )  ",
])
def test_license_expr_error(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="is not a valid"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize('invalid_expr', [
    "",
    " ",
    "\t",
    "\r",
    "\n",
    "\f",
    " \t \n \r \f ",
])
def test_license_expr_error_empty(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="must not be empty"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize('invalid_expr', [
    "mit or mit",
    "or",
    "and",
    "MIT and MIT",
    "MIT AND MIT or MIT",
    "MIT AND (MIT or MIT)",
])
def test_license_expr_error_lowercase(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="must be uppercase"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize('invalid_expr', [
    "WITH",
    "with",
    "WiTh",
    "wiTH",
    "MIT WITH MIT-Exception",
    "(MIT WITH MIT-Exception)",
    "MIT OR MIT WITH MIT-Exception",
    "MIT WITH MIT-Exception OR (MIT AND MIT)",
])
def test_license_expr_error_unsupported_with(invalid_expr: str):
    proj = {
        'name': 'module1', 'version': '1.0', 'description': 'x',
        'license': invalid_expr,
    }
    with pytest.raises(config.ConfigError, match="not yet supported"):
        config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


def test_license_file_defaults_with_old_metadata():
    metadata = {'module': 'mymod', 'author': ''}
    info = config._prep_metadata(metadata, samples_dir / 'pep621_license_files' / 'pyproject.toml')
    assert info.metadata['license_files'] == ["LICENSE"]


@pytest.mark.parametrize(('proj_license_files', 'files'), [
    ({}, ["LICENSE"]),  # Only match default patterns
    ({'license-files': []}, []),
    ({'license-files': ["LICENSE"]}, ["LICENSE"]),
    ({'license-files': ["LICENSE*"]}, ["LICENSE"]),
    ({'license-files': ["LICEN[CS]E*"]}, ["LICENSE"]),
    ({'license-files': ["**/LICENSE*"]}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
    ({'license-files': ["module/vendor/LICENSE*"]}, ["module/vendor/LICENSE_VENDOR"]),
    ({'license-files': ["LICENSE", "module/**/LICENSE*"]}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
    # Add project.license.file + match default patterns
    ({'license': {'file': 'module/vendor/LICENSE_VENDOR'}}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
])
def test_pep621_license_files(proj_license_files, files):
    proj = {'name': 'module1', 'version': '1.0', 'description': 'x'}
    proj.update(proj_license_files)
    info = config.read_pep621_metadata(proj, samples_dir / 'pep621_license_files' / 'pyproject.toml')
    assert info.metadata['license_files'] == files