reprospect 0.0.7__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 (48) hide show
  1. reprospect-0.0.7/LICENSE +21 -0
  2. reprospect-0.0.7/PKG-INFO +24 -0
  3. reprospect-0.0.7/README.md +11 -0
  4. reprospect-0.0.7/pyproject.toml +107 -0
  5. reprospect-0.0.7/reprospect/__init__.py +6 -0
  6. reprospect-0.0.7/reprospect/installers/cuda_bindings.py +101 -0
  7. reprospect-0.0.7/reprospect/installers/nsight_systems.py +75 -0
  8. reprospect-0.0.7/reprospect/test/__init__.py +0 -0
  9. reprospect-0.0.7/reprospect/test/case.py +44 -0
  10. reprospect-0.0.7/reprospect/test/cmake.py +99 -0
  11. reprospect-0.0.7/reprospect/test/environment.py +97 -0
  12. reprospect-0.0.7/reprospect/test/sass/__init__.py +0 -0
  13. reprospect-0.0.7/reprospect/test/sass/composite.py +141 -0
  14. reprospect-0.0.7/reprospect/test/sass/composite_impl.py +223 -0
  15. reprospect-0.0.7/reprospect/test/sass/instruction.py +899 -0
  16. reprospect-0.0.7/reprospect/test/sass/matchers/__init__.py +0 -0
  17. reprospect-0.0.7/reprospect/test/sass/matchers/add_int128.py +204 -0
  18. reprospect-0.0.7/reprospect/tools/__init__.py +0 -0
  19. reprospect-0.0.7/reprospect/tools/architecture.py +189 -0
  20. reprospect-0.0.7/reprospect/tools/binaries/__init__.py +15 -0
  21. reprospect-0.0.7/reprospect/tools/binaries/cuobjdump.py +354 -0
  22. reprospect-0.0.7/reprospect/tools/binaries/demangle.py +45 -0
  23. reprospect-0.0.7/reprospect/tools/binaries/elf.py +398 -0
  24. reprospect-0.0.7/reprospect/tools/binaries/nvdisasm.py +304 -0
  25. reprospect-0.0.7/reprospect/tools/cacher.py +146 -0
  26. reprospect-0.0.7/reprospect/tools/device_properties.py +187 -0
  27. reprospect-0.0.7/reprospect/tools/ncu.py +1098 -0
  28. reprospect-0.0.7/reprospect/tools/nsys.py +565 -0
  29. reprospect-0.0.7/reprospect/tools/sass/__init__.py +9 -0
  30. reprospect-0.0.7/reprospect/tools/sass/controlflow.py +224 -0
  31. reprospect-0.0.7/reprospect/tools/sass/decode.py +338 -0
  32. reprospect-0.0.7/reprospect/utils/__init__.py +0 -0
  33. reprospect-0.0.7/reprospect/utils/cmake.py +116 -0
  34. reprospect-0.0.7/reprospect/utils/compile_command.py +27 -0
  35. reprospect-0.0.7/reprospect/utils/detect.py +97 -0
  36. reprospect-0.0.7/reprospect/utils/ldd.py +19 -0
  37. reprospect-0.0.7/reprospect/utils/nvcc.py +18 -0
  38. reprospect-0.0.7/reprospect/utils/rich_helpers.py +87 -0
  39. reprospect-0.0.7/reprospect/utils/subprocess_helpers.py +54 -0
  40. reprospect-0.0.7/reprospect.egg-info/PKG-INFO +24 -0
  41. reprospect-0.0.7/reprospect.egg-info/SOURCES.txt +46 -0
  42. reprospect-0.0.7/reprospect.egg-info/dependency_links.txt +1 -0
  43. reprospect-0.0.7/reprospect.egg-info/entry_points.txt +4 -0
  44. reprospect-0.0.7/reprospect.egg-info/requires.txt +14 -0
  45. reprospect-0.0.7/reprospect.egg-info/top_level.txt +1 -0
  46. reprospect-0.0.7/requirements/requirements.txt +14 -0
  47. reprospect-0.0.7/setup.cfg +4 -0
  48. reprospect-0.0.7/setup.py +58 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ULiege CSM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: reprospect
3
+ Version: 0.0.7
4
+ Summary: A framework for reproducible prospecting of CUDA applications.
5
+ Author: Arnst Maarten, Tomasetti Romin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/uliegecsm/reprospect
8
+ Requires-Python: >=3.10
9
+ License-File: LICENSE
10
+ Requires-Dist: attrs
11
+ Requires-Dist: backports.strenum
12
+ Requires-Dist: blake3
13
+ Requires-Dist: cmake-file-api==0.0.8.3
14
+ Requires-Dist: ijson
15
+ Requires-Dist: mypy_extensions
16
+ Requires-Dist: pandas
17
+ Requires-Dist: pyelftools
18
+ Requires-Dist: regex
19
+ Requires-Dist: rich
20
+ Requires-Dist: rich-tools
21
+ Requires-Dist: semantic_version
22
+ Requires-Dist: system-helpers>=0.0.3
23
+ Requires-Dist: typing-extensions>=4.4.0
24
+ Dynamic: license-file
@@ -0,0 +1,11 @@
1
+ ![logo](docs/source/_static/logo.svg)
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/reprospect?color=blue)](https://pypi.org/project/reprospect/)
4
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/reprospect)
5
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/reprospect)
6
+
7
+ # reprospect
8
+
9
+ `ReProspect` provides a framework and tools for reproducible prospecting of CUDA applications.
10
+
11
+ See [the documentation](https://uliegecsm.github.io/reprospect/index.html) for more information.
@@ -0,0 +1,107 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools",
4
+ "mypy[mypyc]==1.19.0",
5
+
6
+ # mypyc requires that runtime dependencies are also installed
7
+ # during the build, since it parses the code base.
8
+ # For now, the only solution is to copy-paste the list here.
9
+ "attrs",
10
+ "backports.strenum",
11
+ "blake3",
12
+ "cmake-file-api==0.0.8.3",
13
+ "ijson",
14
+ "mypy_extensions",
15
+ "pandas",
16
+ "pyelftools",
17
+ "regex",
18
+ "rich",
19
+ "rich-tools",
20
+ "semantic_version",
21
+ "system-helpers>=0.0.3",
22
+ "typing-extensions>=4.4.0",
23
+
24
+ # Requirements from requirements/requirements.python.txt that are required by mypyc.
25
+ "types-regex",
26
+ ]
27
+ build-backend = "setuptools.build_meta"
28
+
29
+ [tool.setuptools]
30
+ packages = [
31
+ "reprospect",
32
+ "reprospect.installers",
33
+ "reprospect.test",
34
+ "reprospect.test.sass",
35
+ "reprospect.test.sass.matchers",
36
+ "reprospect.tools",
37
+ "reprospect.tools.binaries",
38
+ "reprospect.tools.sass",
39
+ "reprospect.utils",
40
+ ]
41
+
42
+ [tool.setuptools.dynamic]
43
+ # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#dynamic-metadata
44
+ dependencies = {file = ["requirements/requirements.txt"]}
45
+
46
+ [project]
47
+ name = "reprospect"
48
+ description = "A framework for reproducible prospecting of CUDA applications."
49
+ version = "0.0.7"
50
+ license = "MIT"
51
+ license-files = ["LICENSE"]
52
+ requires-python = ">= 3.10"
53
+ dynamic = [
54
+ "dependencies",
55
+ ]
56
+ authors = [
57
+ {name = "Arnst Maarten"},
58
+ {name = "Tomasetti Romin"},
59
+ ]
60
+
61
+ [project.urls]
62
+ Homepage = "https://github.com/uliegecsm/reprospect"
63
+
64
+ [project.scripts]
65
+ reprospect-install-cuda-bindings = "reprospect.installers.cuda_bindings:main"
66
+ reprospect-install-nsight-systems = "reprospect.installers.nsight_systems:main"
67
+ reprospect-utils-detect-gpus = "reprospect.utils.detect:main"
68
+
69
+ [tool.mypy]
70
+ follow_untyped_imports = true
71
+ warn_unreachable = true
72
+ warn_unused_configs = true
73
+ warn_unused_ignores = true
74
+
75
+ packages = ["reprospect"]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ "docs/source/conf.py" = ["E402"]
79
+
80
+ [tool.pylint.main]
81
+ fail-on = [
82
+ "I",
83
+ ]
84
+ fail-under = 10
85
+
86
+ [tool.pylint."messages control"]
87
+ enable = [
88
+ "useless-suppression",
89
+ ]
90
+ disable = [
91
+ "c-extension-no-member",
92
+ "line-too-long",
93
+ "logging-fstring-interpolation",
94
+ "missing-class-docstring",
95
+ "missing-function-docstring",
96
+ "missing-module-docstring",
97
+ "too-few-public-methods",
98
+ "too-many-arguments",
99
+ "too-many-locals",
100
+ "too-many-positional-arguments",
101
+ ]
102
+
103
+ [tool.cibuildwheel]
104
+ build = []
105
+ archs = []
106
+
107
+ build-verbosity = 1
@@ -0,0 +1,6 @@
1
+ from .test.case import TestCase, CMakeAwareTestCase
2
+
3
+ __all__ = [
4
+ 'CMakeAwareTestCase',
5
+ 'TestCase',
6
+ ]
@@ -0,0 +1,101 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import urllib.request
7
+
8
+ import packaging.version
9
+
10
+ def get_available(package : str) -> list[packaging.version.Version]:
11
+ """
12
+ Get available versions of `package` from `PyPI`.
13
+ """
14
+ with urllib.request.urlopen(f'https://pypi.org/pypi/{package}/json') as resp:
15
+ data = json.load(resp)
16
+ return [packaging.version.Version(x) for x in data["releases"]]
17
+
18
+ def get_candidate(*, target : packaging.version.Version, versions : list[packaging.version.Version]) -> packaging.version.Version | None:
19
+ """
20
+ Match exactly if possible; otherwise, match the closest lower patch version that has the same major and minor.
21
+ """
22
+ candidate = None
23
+
24
+ for version in versions:
25
+ # Reject pre/dev/post releases.
26
+ if version.pre or version.dev or version.post:
27
+ continue
28
+
29
+ # Exact match.
30
+ if version == target:
31
+ return version
32
+
33
+ if version.major == target.major and version.minor == target.minor:
34
+ if version < target and (candidate is None or version > candidate):
35
+ candidate = version
36
+ return candidate
37
+
38
+ def install_cuda_bindings(target : packaging.version.Version) -> None:
39
+ """
40
+ Pick a version of ``cuda-bindings`` that best matches `target`.
41
+
42
+ .. warning::
43
+
44
+ ``cuda-bindings`` was once part of the ``cuda-python`` package, which is now a meta-package.
45
+ Therefore, installing ``cuda-bindings`` alone might be impossible for some versions.
46
+ For instance, there is no standalone ``cuda-bindings==12.6``, but this version can be installed
47
+ as part of ``cuda-python==12.6``.
48
+
49
+ References:
50
+
51
+ * https://nvidia.github.io/cuda-python/latest/
52
+ * https://nvidia.github.io/cuda-python/cuda-bindings/latest/
53
+ """
54
+ versions = get_available(package = 'cuda-bindings')
55
+ logging.info(f'Available versions for \'cuda-bindings\': {versions}.')
56
+
57
+ candidate = get_candidate(target = target, versions = versions)
58
+
59
+ if candidate is not None:
60
+ logging.info(f'Installing \'cuda-bindings\' {candidate}.')
61
+ requirement = f'cuda-bindings=={candidate}'
62
+ else:
63
+ logging.warning('Could not find a suitable version of \'cuda-bindings\'.')
64
+
65
+ versions = get_available(package = 'cuda-python')
66
+ logging.info(f'Available versions for \'cuda-python\': {versions}.')
67
+
68
+ candidate = get_candidate(target = target, versions = versions)
69
+
70
+ if candidate is None:
71
+ raise RuntimeError('Could not find a suitable version of \'cuda-python\'.')
72
+
73
+ logging.info(f'Installing \'cuda-python\' {candidate}.')
74
+ requirement = f'cuda-python=={candidate}'
75
+
76
+ subprocess.check_call(('pip', 'install', requirement))
77
+
78
+ def parse_args() -> argparse.Namespace:
79
+ """
80
+ Parse CLI arguments.
81
+ """
82
+ parser = argparse.ArgumentParser(
83
+ formatter_class = argparse.ArgumentDefaultsHelpFormatter,
84
+ )
85
+
86
+ parser.add_argument('--version', type = packaging.version.Version, required = False, default = os.environ['CUDA_VERSION'])
87
+
88
+ return parser.parse_args()
89
+
90
+ def main() -> None:
91
+
92
+ logging.basicConfig(level = logging.INFO)
93
+
94
+ args = parse_args()
95
+ logging.info(f'Received arguments: {args}.')
96
+
97
+ install_cuda_bindings(target = args.version)
98
+
99
+ if __name__ == '__main__':
100
+
101
+ main()
@@ -0,0 +1,75 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import typing
8
+
9
+ import semantic_version
10
+
11
+ import system_helpers.apt.install
12
+
13
+ from reprospect.utils import nvcc
14
+
15
+ PACKAGE : typing.Final[str] = 'cuda-nsight-systems'
16
+
17
+ def parse_args() -> argparse.Namespace:
18
+ """
19
+ Parse CLI arguments.
20
+ """
21
+ parser = argparse.ArgumentParser(
22
+ description = "Install Nsight Systems with apt.",
23
+ formatter_class = argparse.ArgumentDefaultsHelpFormatter,
24
+ )
25
+
26
+ parser.add_argument('--version', type = str, required = False, help = f'Version of the package ({PACKAGE}) to install.', default = detect_cuda_version())
27
+
28
+ return parser.parse_args()
29
+
30
+ def install(*, args : argparse.Namespace) -> None:
31
+ """
32
+ Install `Nsight Systems` through `apt`.
33
+ """
34
+ package = PACKAGE + '-' + args.version
35
+ logging.info(f'Installing nsight-systems with {package}.')
36
+ system_helpers.apt.install.install_packages(
37
+ packages = [package],
38
+ update = True, clean = True,
39
+ )
40
+
41
+ def detect_cuda_version() -> str:
42
+ """
43
+ Detect CUDA version using the following strategies:
44
+
45
+ #. ``CUDA_VERSION`` environment variable
46
+ #. ``nvidia-smi --query``
47
+ #. ``nvcc --version``
48
+ """
49
+ if 'CUDA_VERSION' in os.environ:
50
+ version = semantic_version.Version(os.environ['CUDA_VERSION'])
51
+ return f'{version.major}-{version.minor}'
52
+
53
+ if shutil.which('nvidia-smi') is not None:
54
+ for line in subprocess.check_output(('nvidia-smi', '--version')).decode().splitlines():
55
+ if "CUDA Version" in line and (matched := re.search(pattern = r'([0-9]+).([0-9]+)', string = line)) is not None:
56
+ return f'{matched.group(1)}-{matched.group(2)}'
57
+
58
+ if shutil.which('nvcc') is not None:
59
+ version = nvcc.get_version()
60
+ return f'{version.major}-{version.minor}'
61
+
62
+ raise RuntimeError("Could not deduce CUDA version.")
63
+
64
+ def main() -> None:
65
+
66
+ logging.basicConfig(level = logging.INFO)
67
+
68
+ args = parse_args()
69
+ logging.info(f"Received arguments: {args}")
70
+
71
+ install(args = args)
72
+
73
+ if __name__ == "__main__":
74
+
75
+ main()
File without changes
@@ -0,0 +1,44 @@
1
+ import abc
2
+ import pathlib
3
+ import typing
4
+
5
+ from reprospect.tools.architecture import NVIDIAArch
6
+ from reprospect.tools.binaries import CuppFilt, LlvmCppFilt
7
+ from reprospect.test.cmake import CMakeMixin
8
+
9
+ class TestCase(abc.ABC):
10
+ """
11
+ Test case.
12
+ """
13
+ @property
14
+ @abc.abstractmethod
15
+ def cwd(self) -> pathlib.Path:
16
+ """
17
+ Working directory.
18
+ """
19
+
20
+ @property
21
+ @abc.abstractmethod
22
+ def arch(self) -> NVIDIAArch:
23
+ """
24
+ NVIDIA architecture.
25
+ """
26
+
27
+ @property
28
+ @abc.abstractmethod
29
+ def executable(self) -> pathlib.Path:
30
+ """
31
+ Executable.
32
+ """
33
+
34
+ @property
35
+ @abc.abstractmethod
36
+ def demangler(self) -> typing.Type[CuppFilt | LlvmCppFilt]:
37
+ """
38
+ Demangler.
39
+ """
40
+
41
+ class CMakeAwareTestCase(CMakeMixin, TestCase):
42
+ """
43
+ Test case with CMake integration.
44
+ """
@@ -0,0 +1,99 @@
1
+ import abc
2
+ import functools
3
+ import json
4
+ import pathlib
5
+ import typing
6
+
7
+ from reprospect.test.environment import EnvironmentField
8
+ from reprospect.tools.architecture import NVIDIAArch
9
+ from reprospect.tools.binaries import CuppFilt, LlvmCppFilt
10
+ from reprospect.utils import cmake
11
+ from reprospect.utils.compile_command import get_arch_from_compile_command
12
+
13
+ def get_demangler_for_compiler(compiler_id : str) -> typing.Type[CuppFilt | LlvmCppFilt]:
14
+ """
15
+ Get demangler for compiler with given id.
16
+ """
17
+ match compiler_id:
18
+ case 'NVIDIA':
19
+ return CuppFilt
20
+ case 'Clang':
21
+ return LlvmCppFilt
22
+ case _:
23
+ raise ValueError(f'unsupported compiler ID {compiler_id}')
24
+
25
+ class CMakeMixin(abc.ABC):
26
+ """
27
+ Mixin to integrate with CMake build system.
28
+ """
29
+ CMAKE_BINARY_DIR = EnvironmentField(converter = pathlib.Path)
30
+ CMAKE_CURRENT_BINARY_DIR = EnvironmentField(converter = pathlib.Path)
31
+
32
+ @classmethod
33
+ @abc.abstractmethod
34
+ def get_target_name(cls) -> str:
35
+ pass
36
+
37
+ @functools.cached_property
38
+ def cwd(self) -> pathlib.Path:
39
+ """
40
+ Get working directory for the analysis based on the CMake current binary directory.
41
+ """
42
+ cwd = self.CMAKE_CURRENT_BINARY_DIR / (self.get_target_name() + '-case')
43
+ cwd.mkdir(parents = False, exist_ok = True)
44
+ return cwd
45
+
46
+ @functools.cached_property
47
+ def arch(self) -> NVIDIAArch:
48
+ """
49
+ Retrieve the NVIDIA architecture from the CMake compile command database.
50
+
51
+ We assume the target file was compiled for only a single architecture.
52
+
53
+ See also https://cmake.org/cmake/help/latest/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html.
54
+ """
55
+ with open(self.CMAKE_BINARY_DIR / 'compile_commands.json', 'r', encoding = 'utf-8') as fin:
56
+ commands = json.load(fin)
57
+ target_source = next(self.target_sources)
58
+ archs = get_arch_from_compile_command(cmd = next(filter(
59
+ lambda x: str(target_source) in x['file'],
60
+ commands))['command']
61
+ )
62
+ assert len(archs) == 1
63
+ return archs.pop()
64
+
65
+ @functools.cached_property
66
+ def cmake_file_api(self) -> cmake.FileAPI:
67
+ return cmake.FileAPI(cmake_build_directory = self.CMAKE_BINARY_DIR)
68
+
69
+ @functools.cached_property
70
+ def target(self) -> cmake.TargetDict:
71
+ """
72
+ Retrieve the target information from the CMake codemodel database.
73
+ """
74
+ return self.cmake_file_api.target(self.get_target_name())
75
+
76
+ @functools.cached_property
77
+ def target_sources(self) -> typing.Iterator[pathlib.Path]:
78
+ """
79
+ Retrieve the target source from the CMake codemodel database.
80
+ """
81
+ return map(lambda source: pathlib.Path(source['path']), self.target['sources'])
82
+
83
+ @functools.cached_property
84
+ def executable(self) -> pathlib.Path:
85
+ """
86
+ Retrieve the executable for the analysis from the CMake codemodel database.
87
+ """
88
+ return self.CMAKE_BINARY_DIR / self.target['paths']['build'] / self.target['nameOnDisk']
89
+
90
+ @functools.cached_property
91
+ def toolchains(self) -> cmake.ToolchainDict:
92
+ """
93
+ Retrieve the toolchains information from the read CMake file API.
94
+ """
95
+ return self.cmake_file_api.toolchains
96
+
97
+ @functools.cached_property
98
+ def demangler(self) -> typing.Type[CuppFilt | LlvmCppFilt]:
99
+ return get_demangler_for_compiler(self.toolchains['CUDA']['compiler']['id'])
@@ -0,0 +1,97 @@
1
+ import dataclasses
2
+ import os
3
+ import typing
4
+
5
+ T = typing.TypeVar('T')
6
+ """Type variable for :py:class:`~EnvironmentField` and related generics."""
7
+
8
+ @dataclasses.dataclass(slots = True)
9
+ class EnvironmentField(typing.Generic[T]):
10
+ """
11
+ Descriptor that returns a value lazily read from an environment variable.
12
+
13
+ Based on:
14
+
15
+ * https://docs.python.org/3/howto/descriptor.html
16
+ * https://mypy.readthedocs.io/en/stable/generics.html#defining-generic-classes
17
+ """
18
+ env : typing.Optional[str] = None
19
+ """
20
+ Name of the environment variable.
21
+ """
22
+
23
+ converter : typing.Optional[typing.Callable[[str], T]] = None
24
+ """
25
+ Callable to convert the value of the environment variable to the target type.
26
+ """
27
+
28
+ default : typing.Optional[T] = None
29
+ """
30
+ Default value if the environment variable does not exist.
31
+ """
32
+
33
+ _cached : T | None = dataclasses.field(default = None, init = False, repr = False)
34
+ """
35
+ Value, cached.
36
+ """
37
+
38
+ _attr_name : str | None = dataclasses.field(default = None, init = False, repr = False)
39
+ """
40
+ Name of the attribute.
41
+ """
42
+
43
+ def __post_init__(self) -> None:
44
+ if self.default is not None and self.converter is None:
45
+ self.converter = type(self.default)
46
+
47
+ def __set_name__(self, owner : type, name: str) -> None:
48
+ """
49
+ References:
50
+
51
+ * https://docs.python.org/3/reference/datamodel.html#object.__set_name__
52
+ """
53
+ self._attr_name = name
54
+
55
+ @typing.overload
56
+ def __get__(self, instance: None, owner: type) -> "EnvironmentField[T]": ...
57
+
58
+ @typing.overload
59
+ def __get__(self, instance: object, owner: type) -> T: ...
60
+
61
+ def __get__(self, instance, owner = None):
62
+ """
63
+ References:
64
+
65
+ * https://docs.python.org/3/reference/datamodel.html#object.__get__
66
+ """
67
+ if instance is None:
68
+ return self
69
+
70
+ return self.read(instance = instance, owner = owner)
71
+
72
+ def read(self, instance, owner) -> T:
73
+ """
74
+ Read from the environment.
75
+ """
76
+ if self._cached is not None:
77
+ return self._cached
78
+
79
+ key = self.env or self._attr_name
80
+
81
+ if key is None:
82
+ raise AttributeError("Descriptor not initialized properly.")
83
+
84
+ if (value := os.getenv(key)) is not None and self.converter is not None:
85
+ self._cached = self.converter(value)
86
+ elif self.default is not None:
87
+ self._cached = self.default
88
+ else:
89
+ raise RuntimeError(f"Missing required environment variable {key!r} or converter or default value for {instance.__class__ if instance else owner}.")
90
+
91
+ return self._cached
92
+
93
+ def reset(self) -> None:
94
+ """
95
+ Reset, thus forcing a re-read from environment on next access.
96
+ """
97
+ self._cached = None
File without changes