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.
Files changed (41) hide show
  1. {python_utils-4.2.0 → python_utils-4.2.1}/PKG-INFO +2 -1
  2. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_import.py +103 -0
  3. {python_utils-4.2.0 → python_utils-4.2.1}/pyproject.toml +3 -1
  4. {python_utils-4.2.0 → python_utils-4.2.1}/pyproject.toml.orig +5 -1
  5. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/import_.py +37 -17
  6. {python_utils-4.2.0 → python_utils-4.2.1}/LICENSE +0 -0
  7. {python_utils-4.2.0 → python_utils-4.2.1}/README.md +0 -0
  8. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/__init__.py +0 -0
  9. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/clock.py +0 -0
  10. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_aio.py +0 -0
  11. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_aliases.py +0 -0
  12. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_containers.py +0 -0
  13. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_converters.py +0 -0
  14. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_decorators.py +0 -0
  15. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_formatters.py +0 -0
  16. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_generators.py +0 -0
  17. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_import_footprint.py +0 -0
  18. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_lazy_imports.py +0 -0
  19. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_logged.py +0 -0
  20. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_logger.py +0 -0
  21. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_python_utils.py +0 -0
  22. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_time.py +0 -0
  23. {python_utils-4.2.0 → python_utils-4.2.1}/_python_utils_tests/test_types.py +0 -0
  24. {python_utils-4.2.0 → python_utils-4.2.1}/conftest.py +0 -0
  25. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/__about__.py +0 -0
  26. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/__init__.py +0 -0
  27. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/_aliases.py +0 -0
  28. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/aio.py +0 -0
  29. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/containers.py +0 -0
  30. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/converters.py +0 -0
  31. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/decorators.py +0 -0
  32. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/exceptions.py +0 -0
  33. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/formatters.py +0 -0
  34. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/generators.py +0 -0
  35. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/logger.py +0 -0
  36. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/loguru.py +0 -0
  37. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/py.typed +0 -0
  38. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/terminal.py +0 -0
  39. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/time.py +0 -0
  40. {python_utils-4.2.0 → python_utils-4.2.1}/python_utils/types.py +0 -0
  41. {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.0
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.0"
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.0" # static; the single source of truth. Bump via `uv version`.
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
- name_parts = name_parts[1:]
119
- level = 1
120
-
121
- # raise IOError((name, level))
122
- module = __import__(
123
- name=name_parts[0] or '.',
124
- globals=globals_,
125
- locals=locals_,
126
- fromlist=name_parts[1:],
127
- level=max(level, 0),
128
- )
129
-
130
- # Make sure we get the right part of a dotted import (i.e.
131
- # spam.eggs should return eggs, not spam)
132
- for attr in name_parts[1:]:
133
- module = _get_attribute(module, attr, '.'.join(name_parts))
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