soft-deps 0.1.1__py3-none-any.whl

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.
soft_deps/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ._version import __version__
4
+ from ._version import __short_description__
5
+ from ._version import __license__
6
+ from ._version import __author__
7
+ from ._version import __author_email__
8
+ from ._version import __maintainer__
9
+ from ._version import __maintainer_email__
soft_deps/_version.py ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python
2
+ # This file is automatically generated by pywf script
3
+ # based on the content in pyproject.toml file
4
+
5
+ __version__ = "0.1.1"
6
+ __short_description__ = "Elegant optional dependency management for Python - full IDE support when available, graceful degradation when missing."
7
+ __license__ = "MIT"
8
+ __author__ = "Sanhe Hu"
9
+ __author_email__ = "husanhe@email.com"
10
+ __maintainer__ = "Sanhe Hu"
11
+ __maintainer_email__ = "husanhe@email.com"
12
+
13
+ # run this script to print out the version number
14
+ if __name__ == "__main__": # pragma: no cover
15
+ print(__version__)
soft_deps/api.py ADDED
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .impl import MissingDependency
soft_deps/impl.py ADDED
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ soft-deps is a Python pattern for elegant optional dependency management that
5
+ prioritizes developer experience over lazy loading. Unlike true lazy imports,
6
+ soft-deps immediately attempts to import dependencies at module load time -
7
+ if the dependency is installed, you get the real module with full functionality.
8
+ If it's missing, you get a helpful proxy object that provides complete IDE
9
+ type hints and auto-completion, but raises informative error messages only
10
+ when you actually try to use the missing functionality. This approach gives you
11
+ the best of both worlds: seamless development experience with full IDE support
12
+ when dependencies are available, and graceful degradation with clear
13
+ installation guidance when they're not, all while maintaining zero code
14
+ invasiveness in your implementation.
15
+ """
16
+
17
+ from functools import total_ordering
18
+
19
+
20
+ @total_ordering
21
+ class MissingDependency:
22
+ def __init__(
23
+ self,
24
+ name: str,
25
+ error_message: str = "please install it",
26
+ ):
27
+ self.name = name
28
+ self.error_message = error_message
29
+
30
+ def _raise_error(self):
31
+ raise ImportError(f"To use `{self.name}`, {self.error_message}.")
32
+
33
+ def __repr__(self): # pragma: no cover
34
+ self._raise_error()
35
+
36
+ def __getattr__(self, attr: str): # pragma: no cover
37
+ self._raise_error()
38
+
39
+ def __getitem__(self, item): # pragma: no cover
40
+ self._raise_error()
41
+
42
+ def __call__(self, *args, **kwargs): # pragma: no cover
43
+ self._raise_error()
44
+
45
+ def __iter__(self): # pragma: no cover
46
+ self._raise_error()
47
+
48
+ def __eq__(self, other): # pragma: no cover
49
+ self._raise_error()
50
+
51
+ def __lt__(self, other): # pragma: no cover
52
+ self._raise_error()
53
+
54
+ def __hash__(self): # pragma: no cover
55
+ self._raise_error()
56
+
57
+ def __add__(self, other): # pragma: no cover
58
+ self._raise_error()
59
+
60
+ def __sub__(self, other): # pragma: no cover
61
+ self._raise_error()
62
+
63
+ def __mul__(self, other): # pragma: no cover
64
+ self._raise_error()
65
+
66
+ def __truediv__(self, other): # pragma: no cover
67
+ self._raise_error()
soft_deps/paths.py ADDED
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from pathlib import Path
4
+
5
+ dir_here = Path(__file__).absolute().parent
6
+ dir_package = dir_here
7
+ PACKAGE_NAME = dir_package.name
8
+
9
+ dir_project_root = dir_package.parent
10
+
11
+ # ------------------------------------------------------------------------------
12
+ # Virtual Environment Related
13
+ # ------------------------------------------------------------------------------
14
+ dir_venv = dir_project_root / ".venv"
15
+ dir_venv_bin = dir_venv / "bin"
16
+
17
+ # virtualenv executable paths
18
+ bin_pytest = dir_venv_bin / "pytest"
19
+
20
+ # ------------------------------------------------------------------------------
21
+ # Test Related
22
+ # ------------------------------------------------------------------------------
23
+ dir_htmlcov = dir_project_root / "htmlcov"
24
+ path_cov_index_html = dir_htmlcov / "index.html"
25
+ dir_unit_test = dir_project_root / "tests"
26
+ dir_int_test = dir_project_root / "tests_int"
27
+ dir_load_test = dir_project_root / "tests_load"
28
+
29
+ # ------------------------------------------------------------------------------
30
+ # Doc Related
31
+ # ------------------------------------------------------------------------------
32
+ dir_docs_source = dir_project_root / "docs" / "source"
33
+ dir_docs_build_html = dir_project_root / "docs" / "build" / "html"
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import sys
5
+ import contextlib
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ __version__ = "0.2.1"
10
+
11
+
12
+ @contextlib.contextmanager
13
+ def temp_cwd(path: Path):
14
+ """
15
+ Temporarily set the current working directory (CWD) and automatically
16
+ switch back when it's done.
17
+ """
18
+ cwd = os.getcwd()
19
+ os.chdir(str(path))
20
+ try:
21
+ yield path
22
+ finally:
23
+ os.chdir(cwd)
24
+
25
+
26
+ def run_unit_test(
27
+ script: str,
28
+ root_dir: str,
29
+ ):
30
+ """
31
+ Run ``pytest -s --tb=native /path/to/script.py`` Command.
32
+
33
+ :param script: the path to test script
34
+ :param root_dir: the dir you want to temporarily set as cwd
35
+ """
36
+ bin_pytest = Path(sys.executable).parent / "pytest"
37
+ args = [
38
+ f"{bin_pytest}",
39
+ "-s",
40
+ "--tb=native",
41
+ script,
42
+ ]
43
+ with temp_cwd(Path(root_dir)):
44
+ subprocess.run(args)
45
+
46
+
47
+ def run_cov_test(
48
+ script: str,
49
+ module: str,
50
+ root_dir: str,
51
+ htmlcov_dir: str,
52
+ preview: bool = False,
53
+ is_folder: bool = False,
54
+ ):
55
+ """
56
+ The pytest-cov plugin gives you the coverage for entire project. What if
57
+ I want run per-module test independently and get per-module coverage?
58
+
59
+ This is a simple wrapper around pytest + coverage cli command. Allow you to run
60
+ coverage test from Python script and set the code coverage measurement scope.
61
+
62
+ Usage example:
63
+
64
+ suppose you have a source code folder structure like this::
65
+
66
+ /dir_git_repo/
67
+ /dir_git_repo/my_library
68
+ /dir_git_repo/my_library/__init__.py
69
+ /dir_git_repo/my_library/module1.py
70
+ /dir_git_repo/my_library/module2.py
71
+
72
+ In your module 1 unit test script, you can do this:
73
+
74
+ .. code-block:: python
75
+
76
+ from my_library.module1 import func1, func2
77
+
78
+ def test_func1():
79
+ pass
80
+
81
+ def test_func2():
82
+ pass
83
+
84
+ if __name__ == "__main__":
85
+ from fixa.pytest_cov_helper import run_cov_test
86
+
87
+ run_cov_test(
88
+ script=__file__,
89
+ module="my_library.module1", # test scope is the module1.py
90
+ root_dir="/path/to/dir_git_repo",
91
+ htmlcov_dir="/path/to/dir_git_repo/htmlcov",
92
+ )
93
+
94
+ In your all modules unit test script, you can do this:
95
+
96
+ .. code-block:: python
97
+
98
+ if __name__ == "__main__":
99
+ from fixa.pytest_cov_helper import run_cov_test
100
+
101
+ run_cov_test(
102
+ script=__file__,
103
+ module="my_library", # test scope is the my_library/
104
+ root_dir="/path/to/dir_git_repo",
105
+ htmlcov_dir="/path/to/dir_git_repo/htmlcov",
106
+ is_folder=True, # my_library is a folder
107
+ )
108
+
109
+ :param script: the test script absolute path
110
+ :param module: the dot notation to the python module you want to calculate
111
+ coverage
112
+ :param root_dir: the dir to dump coverage results binary file
113
+ :param htmlcov_dir: the dir to dump HTML output
114
+ :param preview: whether to open the HTML output in web browser after the test
115
+ :param is_folder: whether the module is a folder
116
+
117
+ Reference:
118
+
119
+ - https://pypi.org/project/pytest-cov/
120
+ """
121
+ bin_pytest = Path(sys.executable).parent / "pytest"
122
+ if is_folder:
123
+ script = f"{Path(script).parent}"
124
+ if module.endswith(".py"): # pragma: no cover
125
+ module = module[:-3]
126
+ args = [
127
+ f"{bin_pytest}",
128
+ "-s",
129
+ "--tb=native",
130
+ f"--rootdir={root_dir}",
131
+ f"--cov={module}",
132
+ "--cov-report",
133
+ "term-missing",
134
+ "--cov-report",
135
+ f"html:{htmlcov_dir}",
136
+ script,
137
+ ]
138
+ with temp_cwd(Path(root_dir)):
139
+ subprocess.run(args)
140
+ if preview: # pragma: no cover
141
+ platform = sys.platform
142
+ if platform in ["win32", "cygwin"]:
143
+ open_command = "start"
144
+ elif platform in ["darwin", "linux"]:
145
+ open_command = "open"
146
+ else:
147
+ raise NotImplementedError
148
+ subprocess.run([open_command, f"{Path(htmlcov_dir).joinpath('index.html')}"])
@@ -0,0 +1,15 @@
1
+ .. _about_author:
2
+
3
+ About the Author
4
+ ------------------------------------------------------------------------------
5
+ ::
6
+
7
+ (\ (\
8
+ ( -.-)o
9
+ o_(")(")
10
+
11
+ **Sanhe Hu** is a seasoned software engineer with a deep passion for Python development since 2010. As an author and maintainer of `150+ open-source Python projects <https://pypi.org/user/machugwu/>`_, with over `15 million monthly downloads <https://github.com/MacHu-GWU>`_, I bring a wealth of experience to the table. As a Senior Solution Architect and Subject Matter Expert in AI, Data, Amazon Web Services, Cloud Engineering, DevOps, I thrive on helping clients with platform design, enterprise architecture, and strategic roadmaps.
12
+
13
+ Talk is cheap, show me the code:
14
+
15
+ - My Github: https://github.com/MacHu-GWU
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sanhe Hu <husanhe@email.com>
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,192 @@
1
+ Metadata-Version: 2.3
2
+ Name: soft_deps
3
+ Version: 0.1.1
4
+ Summary: Elegant optional dependency management for Python - full IDE support when available, graceful degradation when missing.
5
+ License: MIT
6
+ Author: Sanhe Hu
7
+ Author-email: husanhe@email.com
8
+ Maintainer: Sanhe Hu
9
+ Maintainer-email: husanhe@email.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Provides-Extra: auto
19
+ Provides-Extra: dev
20
+ Provides-Extra: doc
21
+ Provides-Extra: test
22
+ Requires-Dist: Sphinx (>=7.4.7,<8.0.0) ; extra == "doc"
23
+ Requires-Dist: build (>=1.2.1,<2.0.0) ; extra == "dev"
24
+ Requires-Dist: docfly (==3.0.0) ; extra == "doc"
25
+ Requires-Dist: furo (==2024.8.6) ; extra == "doc"
26
+ Requires-Dist: ipython (>=8.18.1,<8.19.0) ; extra == "doc"
27
+ Requires-Dist: nbsphinx (>=0.8.12,<1.0.0) ; extra == "doc"
28
+ Requires-Dist: pygments (>=2.18.0,<3.0.0) ; extra == "doc"
29
+ Requires-Dist: pytest (>=8.2.2,<9.0.0) ; extra == "test"
30
+ Requires-Dist: pytest-cov (>=6.0.0,<7.0.0) ; extra == "test"
31
+ Requires-Dist: rich (>=13.8.1,<14.0.0) ; extra == "dev"
32
+ Requires-Dist: rstobj (==1.2.1) ; extra == "doc"
33
+ Requires-Dist: sphinx-copybutton (>=0.5.2,<1.0.0) ; extra == "doc"
34
+ Requires-Dist: sphinx-design (>=0.6.1,<1.0.0) ; extra == "doc"
35
+ Requires-Dist: sphinx-jinja (>=2.0.2,<3.0.0) ; extra == "doc"
36
+ Requires-Dist: twine (>=6.0.0,<7.0.0) ; extra == "dev"
37
+ Requires-Dist: wheel (>=0.45.0,<1.0.0) ; extra == "dev"
38
+ Project-URL: Changelog, https://github.com/MacHu-GWU/soft_deps-project/blob/main/release-history.rst
39
+ Project-URL: Documentation, https://soft-deps.readthedocs.io/en/latest/
40
+ Project-URL: Download, https://pypi.org/pypi/soft-deps#files
41
+ Project-URL: Homepage, https://github.com/MacHu-GWU/soft_deps-project
42
+ Project-URL: Issues, https://github.com/MacHu-GWU/soft_deps-project/issues
43
+ Project-URL: Repository, https://github.com/MacHu-GWU/soft_deps-project
44
+ Description-Content-Type: text/x-rst
45
+
46
+
47
+ .. image:: https://readthedocs.org/projects/soft-deps/badge/?version=latest
48
+ :target: https://soft-deps.readthedocs.io/en/latest/
49
+ :alt: Documentation Status
50
+
51
+ .. image:: https://github.com/MacHu-GWU/soft_deps-project/actions/workflows/main.yml/badge.svg
52
+ :target: https://github.com/MacHu-GWU/soft_deps-project/actions?query=workflow:CI
53
+
54
+ .. image:: https://codecov.io/gh/MacHu-GWU/soft_deps-project/branch/main/graph/badge.svg
55
+ :target: https://codecov.io/gh/MacHu-GWU/soft_deps-project
56
+
57
+ .. image:: https://img.shields.io/pypi/v/soft-deps.svg
58
+ :target: https://pypi.python.org/pypi/soft-deps
59
+
60
+ .. image:: https://img.shields.io/pypi/l/soft-deps.svg
61
+ :target: https://pypi.python.org/pypi/soft-deps
62
+
63
+ .. image:: https://img.shields.io/pypi/pyversions/soft-deps.svg
64
+ :target: https://pypi.python.org/pypi/soft-deps
65
+
66
+ .. image:: https://img.shields.io/badge/✍️_Release_History!--None.svg?style=social&logo=github
67
+ :target: https://github.com/MacHu-GWU/soft_deps-project/blob/main/release-history.rst
68
+
69
+ .. image:: https://img.shields.io/badge/⭐_Star_me_on_GitHub!--None.svg?style=social&logo=github
70
+ :target: https://github.com/MacHu-GWU/soft_deps-project
71
+
72
+ ------
73
+
74
+ .. image:: https://img.shields.io/badge/Link-API-blue.svg
75
+ :target: https://soft-deps.readthedocs.io/en/latest/py-modindex.html
76
+
77
+ .. image:: https://img.shields.io/badge/Link-Install-blue.svg
78
+ :target: `install`_
79
+
80
+ .. image:: https://img.shields.io/badge/Link-GitHub-blue.svg
81
+ :target: https://github.com/MacHu-GWU/soft_deps-project
82
+
83
+ .. image:: https://img.shields.io/badge/Link-Submit_Issue-blue.svg
84
+ :target: https://github.com/MacHu-GWU/soft_deps-project/issues
85
+
86
+ .. image:: https://img.shields.io/badge/Link-Request_Feature-blue.svg
87
+ :target: https://github.com/MacHu-GWU/soft_deps-project/issues
88
+
89
+ .. image:: https://img.shields.io/badge/Link-Download-blue.svg
90
+ :target: https://pypi.org/pypi/soft-deps#files
91
+
92
+
93
+ Welcome to ``soft_deps`` Documentation
94
+ ==============================================================================
95
+ .. image:: https://soft-deps.readthedocs.io/en/latest/_static/soft_deps-logo.png
96
+ :target: https://soft-deps.readthedocs.io/en/latest/
97
+
98
+ soft-deps is a Python pattern for elegant optional dependency management that prioritizes developer experience over lazy loading. Unlike true lazy imports, soft-deps immediately attempts to import dependencies at module load time - if the dependency is installed, you get the real module with full functionality. If it's missing, you get a helpful proxy object that provides complete IDE type hints and auto-completion, but raises informative error messages only when you actually try to use the missing functionality.
99
+
100
+ This approach gives you the best of both worlds: seamless development experience with full IDE support when dependencies are available, and graceful degradation with clear installation guidance when they're not, all while maintaining zero code invasiveness in your implementation.
101
+
102
+
103
+ Quick Start
104
+ ------------------------------------------------------------------------------
105
+ **Why use soft-deps?**
106
+
107
+ - **For library authors**: Provide optional features without forcing users to install heavy dependencies
108
+ - **For users**: Get full IDE support and autocomplete even for optional dependencies
109
+ - **For teams**: Graceful degradation with clear error messages instead of cryptic import failures
110
+
111
+ **When to use soft-deps?**
112
+
113
+ Use soft-deps when your library has optional features that depend on external packages:
114
+
115
+ - Data science libraries with optional visualization (matplotlib, plotly)
116
+ - Web frameworks with optional database drivers (psycopg2, pymongo)
117
+ - CLI tools with optional cloud integrations (boto3, google-cloud)
118
+ - Any library where you want some features to work without heavy dependencies
119
+
120
+ **How it works:**
121
+
122
+ Library authors use the soft-deps pattern in their code:
123
+
124
+ .. code-block:: python
125
+
126
+ # your_library.py
127
+ from soft_deps.api import MissingDependency
128
+
129
+ # Try to import optional dependency
130
+ try:
131
+ import pandas as pd
132
+ except ImportError:
133
+ # Create helpful proxy if missing
134
+ pd = MissingDependency("pandas", "pip install pandas")
135
+
136
+ def export_to_excel(data):
137
+ """Export data to Excel file (requires pandas)."""
138
+ # IDE gets full autocomplete here even if pandas not installed
139
+ # Error only occurs when this line actually executes
140
+ return pd.DataFrame(data).to_excel("output.xlsx")
141
+
142
+ Users get seamless experience regardless of what's installed:
143
+
144
+ .. code-block:: python
145
+
146
+ # user_script.py
147
+ from your_library import export_to_excel
148
+
149
+ # This imports successfully even without pandas installed
150
+ # IDE provides full autocomplete and type hints
151
+
152
+ try:
153
+ export_to_excel({"col1": [1, 2, 3]})
154
+ except ImportError as e:
155
+ print(e) # "You didn't install pandas. To use DataFrame, pip install pandas"
156
+
157
+ **The difference:**
158
+
159
+ .. code-block:: python
160
+
161
+ # ❌ Traditional approach - breaks immediately
162
+ import pandas as pd # ImportError if not installed
163
+
164
+ # ❌ Lazy imports - no IDE support
165
+ def export_data():
166
+ import pandas as pd # IDE can't help with autocomplete
167
+
168
+ # ✅ soft-deps - best of both worlds
169
+ try:
170
+ import pandas as pd
171
+ except ImportError:
172
+ pd = MissingDependency("pandas", "pip install pandas")
173
+ # IDE gets full support, graceful errors when actually used
174
+
175
+
176
+ .. _install:
177
+
178
+ Install
179
+ ------------------------------------------------------------------------------
180
+
181
+ ``soft_deps`` is released on PyPI, so all you need is to:
182
+
183
+ .. code-block:: console
184
+
185
+ $ pip install soft-deps
186
+
187
+ To upgrade to latest version:
188
+
189
+ .. code-block:: console
190
+
191
+ $ pip install --upgrade soft-deps
192
+
@@ -0,0 +1,12 @@
1
+ soft_deps/__init__.py,sha256=91jqwa6UIinipz7S0hF5_qpjjB9MUONExbvbLt7UTys,289
2
+ soft_deps/_version.py,sha256=JyS_-kQsLE61QvN-YNEGQYKtbq80QQ787zSo3Hi2T24,566
3
+ soft_deps/api.py,sha256=i85xEuYiS-Ez_qPopY1XSj9B3gyoSFiawlSq5R_34Lw,61
4
+ soft_deps/impl.py,sha256=oRIEryaZ5Ncnj2X3bdsFq6NM7C6i-5oyohDjSMU2vcM,2147
5
+ soft_deps/paths.py,sha256=JH9o7s__FexYpUMuwV7SArCUCdatooOQA1gUYtBm7UY,1226
6
+ soft_deps/vendor/__init__.py,sha256=O9CT1B2F-cVB8elT0EoCJbgkcffjvlmqteqavs4giDg,25
7
+ soft_deps/vendor/pytest_cov_helper.py,sha256=H2BJtFq3bwJHZtt-bxZ1yF9LnXHVDRZZ0NH-OvkRJdE,4063
8
+ soft_deps-0.1.1.dist-info/AUTHORS.rst,sha256=oo38V9AD_y57Ac69mKmiItWquV29SRi2aTCdKQo531A,767
9
+ soft_deps-0.1.1.dist-info/LICENSE.txt,sha256=Vc2EgX0hR-6Qw_RxS1IR-2qllqh4PiqSWFvrdnqZSDU,1084
10
+ soft_deps-0.1.1.dist-info/METADATA,sha256=HFjOHE9k7LqYwW-e7BgynZmEN75nqkp4lUDJ6K1hPUo,7897
11
+ soft_deps-0.1.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
12
+ soft_deps-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any