python-utils 4.2.0__tar.gz → 4.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_utils-4.2.0 → python_utils-4.2.1}/PKG-INFO +2 -1
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_import.py +103 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/pyproject.toml +3 -1
- {python_utils-4.2.0 → python_utils-4.2.1}/pyproject.toml.orig +5 -1
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/import_.py +37 -17
- {python_utils-4.2.0 → python_utils-4.2.1}/LICENSE +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/README.md +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/__init__.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/clock.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_aio.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_aliases.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_containers.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_converters.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_decorators.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_formatters.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_generators.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_import_footprint.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_lazy_imports.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_logged.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_logger.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_python_utils.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_time.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_types.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/conftest.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/__about__.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/__init__.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/_aliases.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/aio.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/containers.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/converters.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/decorators.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/exceptions.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/formatters.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/generators.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/logger.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/loguru.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/py.typed +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/terminal.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/time.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/types.py +0 -0
- {python_utils-4.2.0 → python_utils-4.2.1}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-utils
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
|
|
5
5
|
Keywords: utils,utilities,helpers,typing,typed,async,lazy-imports
|
|
6
6
|
Author: Rick van Hattem
|
|
@@ -37,6 +37,7 @@ Project-URL: Documentation, https://python-utils.readthedocs.io/
|
|
|
37
37
|
Project-URL: Repository, https://github.com/WoLpH/python-utils
|
|
38
38
|
Project-URL: Changelog, https://github.com/WoLpH/python-utils/releases
|
|
39
39
|
Project-URL: Funding, https://github.com/sponsors/wolph
|
|
40
|
+
Project-URL: Issues, https://github.com/wolph/python-utils/issues
|
|
40
41
|
Provides-Extra: docs
|
|
41
42
|
Provides-Extra: loguru
|
|
42
43
|
Provides-Extra: tests
|
|
@@ -299,3 +299,106 @@ def test_import_global_requested_name_needs_an_export(
|
|
|
299
299
|
import_.import_global(module_name, ['eggs'], locals_={}, globals_=scope)
|
|
300
300
|
assert scope == {}
|
|
301
301
|
assert 'eggs' not in calls
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.fixture
|
|
305
|
+
def deep_package(
|
|
306
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
307
|
+
) -> collections.abc.Iterator[str]:
|
|
308
|
+
"""Create ``testrel.nested.deeper`` with importable helper modules."""
|
|
309
|
+
package: pathlib.Path = tmp_path / 'testrel'
|
|
310
|
+
nested: pathlib.Path = package / 'nested'
|
|
311
|
+
deeper: pathlib.Path = nested / 'deeper'
|
|
312
|
+
deeper.mkdir(parents=True)
|
|
313
|
+
(package / '__init__.py').write_text('', encoding='utf-8')
|
|
314
|
+
(nested / '__init__.py').write_text('', encoding='utf-8')
|
|
315
|
+
(deeper / '__init__.py').write_text('', encoding='utf-8')
|
|
316
|
+
(package / 'helper.py').write_text('value = 42\n', encoding='utf-8')
|
|
317
|
+
(nested / 'inner.py').write_text(
|
|
318
|
+
"inner_value = 'inner'\n", encoding='utf-8'
|
|
319
|
+
)
|
|
320
|
+
monkeypatch.setattr(sys, 'path', [str(tmp_path), *sys.path])
|
|
321
|
+
|
|
322
|
+
yield 'testrel'
|
|
323
|
+
|
|
324
|
+
imported: list[str] = [
|
|
325
|
+
name for name in sys.modules if name.split('.')[0] == 'testrel'
|
|
326
|
+
]
|
|
327
|
+
for name in imported:
|
|
328
|
+
del sys.modules[name]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_import_globals_relative_import_two_dots(deep_package: str) -> None:
|
|
332
|
+
"""Resolve ``..helper`` two levels up, to ``testrel.helper`` (#73)."""
|
|
333
|
+
locals_: types.Dict[str, types.Any] = {}
|
|
334
|
+
globals_: types.Dict[str, types.Any] = {'__package__': 'testrel.nested'}
|
|
335
|
+
import_.import_global('..helper', locals_=locals_, globals_=globals_)
|
|
336
|
+
assert globals_['value'] == 42
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_import_globals_relative_import_three_dots(deep_package: str) -> None:
|
|
340
|
+
"""Resolve ``...helper`` three levels up, to ``testrel.helper`` (#73)."""
|
|
341
|
+
locals_: types.Dict[str, types.Any] = {}
|
|
342
|
+
globals_: types.Dict[str, types.Any] = {
|
|
343
|
+
'__package__': 'testrel.nested.deeper'
|
|
344
|
+
}
|
|
345
|
+
import_.import_global('...helper', locals_=locals_, globals_=globals_)
|
|
346
|
+
assert globals_['value'] == 42
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_import_globals_relative_import_one_dot(deep_package: str) -> None:
|
|
350
|
+
"""Keep resolving a single leading dot one level up (#73 control)."""
|
|
351
|
+
locals_: types.Dict[str, types.Any] = {}
|
|
352
|
+
globals_: types.Dict[str, types.Any] = {'__package__': 'testrel.nested'}
|
|
353
|
+
import_.import_global('.inner', locals_=locals_, globals_=globals_)
|
|
354
|
+
assert globals_['inner_value'] == 'inner'
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_import_globals_relative_import_dots_only(deep_package: str) -> None:
|
|
358
|
+
"""Reject a dots-only name, which names no module to import (#73)."""
|
|
359
|
+
locals_: types.Dict[str, types.Any] = {}
|
|
360
|
+
globals_: types.Dict[str, types.Any] = {'__package__': 'testrel.nested'}
|
|
361
|
+
with pytest.raises(ImportError):
|
|
362
|
+
import_.import_global('..', locals_=locals_, globals_=globals_)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_import_globals_relative_import_dots_only_exception(
|
|
366
|
+
deep_package: str,
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Keep raising ModuleNotFoundError for a dots-only name, as 4.2.0 did."""
|
|
369
|
+
globals_: types.Dict[str, types.Any] = {
|
|
370
|
+
'__package__': 'testrel.nested.deeper'
|
|
371
|
+
}
|
|
372
|
+
error = import_.import_global(
|
|
373
|
+
'...',
|
|
374
|
+
exceptions=ModuleNotFoundError,
|
|
375
|
+
locals_={},
|
|
376
|
+
globals_=globals_,
|
|
377
|
+
)
|
|
378
|
+
assert isinstance(error, ModuleNotFoundError)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_import_globals_relative_import_beyond_top_level(
|
|
382
|
+
deep_package: str,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Raise ModuleNotFoundError above the top-level package, as 4.2.0 did."""
|
|
385
|
+
globals_: types.Dict[str, types.Any] = {
|
|
386
|
+
'__package__': 'testrel.nested.deeper'
|
|
387
|
+
}
|
|
388
|
+
with pytest.raises(ModuleNotFoundError, match='beyond top-level'):
|
|
389
|
+
import_.import_global('....helper', locals_={}, globals_=globals_)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_import_globals_empty_name() -> None:
|
|
393
|
+
"""Keep raising IndexError for an empty name, as 4.2.0 did."""
|
|
394
|
+
with pytest.raises(IndexError):
|
|
395
|
+
import_.import_global('', locals_={}, globals_={'__package__': ''})
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_import_globals_relative_import_trailing_dot(
|
|
399
|
+
deep_package: str,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Raise ModuleNotFoundError for ``..helper.``, as 4.2.0 did."""
|
|
402
|
+
globals_: types.Dict[str, types.Any] = {'__package__': 'testrel.nested'}
|
|
403
|
+
with pytest.raises(ModuleNotFoundError):
|
|
404
|
+
import_.import_global('..helper.', locals_={}, globals_=globals_)
|
|
@@ -70,6 +70,7 @@ doctest_optionflags = [
|
|
|
70
70
|
"ALLOW_BYTES",
|
|
71
71
|
]
|
|
72
72
|
asyncio_mode = "strict"
|
|
73
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
73
74
|
xfail_strict = true
|
|
74
75
|
filterwarnings = [
|
|
75
76
|
"error",
|
|
@@ -125,7 +126,7 @@ source-include = [
|
|
|
125
126
|
|
|
126
127
|
[project]
|
|
127
128
|
name = "python-utils"
|
|
128
|
-
version = "4.2.
|
|
129
|
+
version = "4.2.1"
|
|
129
130
|
description = "Python Utils is a module with some convenient utilities not included with the standard Python install"
|
|
130
131
|
readme = "README.md"
|
|
131
132
|
requires-python = ">=3.10"
|
|
@@ -168,6 +169,7 @@ Documentation = "https://python-utils.readthedocs.io/"
|
|
|
168
169
|
Repository = "https://github.com/WoLpH/python-utils"
|
|
169
170
|
Changelog = "https://github.com/WoLpH/python-utils/releases"
|
|
170
171
|
Funding = "https://github.com/sponsors/wolph"
|
|
172
|
+
Issues = "https://github.com/wolph/python-utils/issues"
|
|
171
173
|
|
|
172
174
|
[project.optional-dependencies]
|
|
173
175
|
loguru = ["loguru"]
|
|
@@ -12,7 +12,7 @@ source-include = ['_python_utils_tests/**/*.py', 'conftest.py', 'tox.ini']
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = 'python-utils'
|
|
15
|
-
version = "4.2.
|
|
15
|
+
version = "4.2.1" # static; the single source of truth. Bump via `uv version`.
|
|
16
16
|
description = 'Python Utils is a module with some convenient utilities not included with the standard Python install'
|
|
17
17
|
readme = 'README.md'
|
|
18
18
|
requires-python = '>=3.10'
|
|
@@ -52,6 +52,7 @@ Documentation = 'https://python-utils.readthedocs.io/'
|
|
|
52
52
|
Repository = 'https://github.com/WoLpH/python-utils'
|
|
53
53
|
Changelog = 'https://github.com/WoLpH/python-utils/releases'
|
|
54
54
|
Funding = 'https://github.com/sponsors/wolph'
|
|
55
|
+
Issues = 'https://github.com/wolph/python-utils/issues'
|
|
55
56
|
|
|
56
57
|
[project.optional-dependencies]
|
|
57
58
|
loguru = ['loguru']
|
|
@@ -124,6 +125,9 @@ addopts = [
|
|
|
124
125
|
]
|
|
125
126
|
doctest_optionflags = ['ALLOW_UNICODE', 'ALLOW_BYTES']
|
|
126
127
|
asyncio_mode = 'strict'
|
|
128
|
+
# pytest-asyncio 1.4 warns when this is unset, and filterwarnings turns the
|
|
129
|
+
# warning into an INTERNALERROR. 'function' is the scope it already used.
|
|
130
|
+
asyncio_default_fixture_loop_scope = 'function'
|
|
127
131
|
xfail_strict = true
|
|
128
132
|
filterwarnings = [
|
|
129
133
|
'error',
|
|
@@ -113,24 +113,44 @@ def import_global( # noqa: C901
|
|
|
113
113
|
globals_ = frame.f_globals
|
|
114
114
|
|
|
115
115
|
try:
|
|
116
|
-
# Relative imports are supported (from .spam import eggs)
|
|
116
|
+
# Relative imports are supported (from .spam import eggs). Each
|
|
117
|
+
# leading dot counts as one level, so ``..spam`` goes up two.
|
|
118
|
+
# An empty or dots-only name keeps the one-level path it had in
|
|
119
|
+
# 4.2.0, and with it the IndexError or ModuleNotFoundError.
|
|
120
|
+
multi_dot: bool = False
|
|
117
121
|
if not name_parts[0]:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
remainder: str = name.lstrip('.')
|
|
123
|
+
dots: int = len(name) - len(remainder)
|
|
124
|
+
if dots > 1 and remainder:
|
|
125
|
+
multi_dot = True
|
|
126
|
+
level = dots
|
|
127
|
+
name_parts = remainder.split('.')
|
|
128
|
+
else:
|
|
129
|
+
name_parts = name_parts[1:]
|
|
130
|
+
level = 1
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
module = __import__(
|
|
134
|
+
name=name_parts[0] or '.',
|
|
135
|
+
globals=globals_,
|
|
136
|
+
locals=locals_,
|
|
137
|
+
fromlist=name_parts[1:],
|
|
138
|
+
level=max(level, 0),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Make sure we get the right part of a dotted import (i.e.
|
|
142
|
+
# spam.eggs should return eggs, not spam)
|
|
143
|
+
for attr in name_parts[1:]:
|
|
144
|
+
module = _get_attribute(module, attr, '.'.join(name_parts))
|
|
145
|
+
except ImportError as error:
|
|
146
|
+
# Every multi-dot name failed with ModuleNotFoundError in
|
|
147
|
+
# 4.2.0. Python raises a plain ImportError for some of them,
|
|
148
|
+
# such as going above the top-level package, which
|
|
149
|
+
# `except ModuleNotFoundError` misses. The subclass is
|
|
150
|
+
# caught by both.
|
|
151
|
+
if not multi_dot or isinstance(error, ModuleNotFoundError):
|
|
152
|
+
raise
|
|
153
|
+
raise ModuleNotFoundError(str(error), name=name) from error
|
|
134
154
|
|
|
135
155
|
# If no list of modules is given, autodetect from either __all__
|
|
136
156
|
# or a dir() of the module
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|