├── tests ├── __init__.py ├── conftest.py ├── test_names.py ├── test_factory.py └── test_singleton.py ├── docs ├── refs.md ├── COLLABORATE.md └── README.md ├── vacuna ├── __version__.py ├── __init__.py └── lazy.py ├── .readthedocs.yaml ├── .gitignore ├── mkdocs.yml ├── tox.ini ├── .github └── workflows │ ├── python-publish.yml │ └── python-package.yml ├── LICENSE ├── pyproject.toml └── examples └── cli.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/refs.md: -------------------------------------------------------------------------------- 1 | ::: vacuna 2 | -------------------------------------------------------------------------------- /vacuna/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.2' 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import vacuna 4 | 5 | 6 | @pytest.fixture(scope='module') 7 | def container(): 8 | """Use this fixture per module to configure your app""" 9 | return vacuna.Container() 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | mkdocs: 4 | configuration: mkdocs.yml 5 | fail_on_warning: true 6 | 7 | python: 8 | version: 3 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__/ 3 | dist/ 4 | 5 | .venv/ 6 | .mypy_cache/ 7 | .pytest_cache/ 8 | .tox/ 9 | 10 | .coverage 11 | coverage.xml 12 | htmlcov/ 13 | .benchmarks/ 14 | 15 | site/ 16 | 17 | .python-version 18 | poetry.lock 19 | 20 | .vscode/ 21 | *.code-workspace 22 | 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /docs/COLLABORATE.md: -------------------------------------------------------------------------------- 1 | # Add an issue 2 | 3 | Be descriptive and what not. 4 | 5 | # Development 6 | 7 | Install development dependencies. 8 | 9 | ```bash 10 | poetry install 11 | ``` 12 | 13 | and test, try to keep your code clean. 14 | 15 | ```bash 16 | poetry run pytest tests 17 | ``` 18 | 19 | # Documentation 20 | 21 | ```bash 22 | poetry install -E docs 23 | ``` 24 | 25 | ```bash 26 | poetry run mkdocs serve 27 | ``` 28 | 29 | # Submit a PR 30 | 31 | ## Create your fork 32 | 33 | Link the issue. 34 | -------------------------------------------------------------------------------- /tests/test_names.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(autouse=True) 5 | def app(container): 6 | class App: 7 | def run(self): 8 | print('this works!') 9 | 10 | return container.dependency(name='app')(App) 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def main(container): 15 | def _main(app): 16 | app.run() 17 | 18 | return container.dependency(name='main')(_main) 19 | 20 | 21 | def test_simple(container, main, capsys): 22 | container.run(main) 23 | 24 | captured = capsys.readouterr() 25 | 26 | assert captured.out == 'this works!\n' 27 | assert captured.err == '' 28 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class App: 5 | pass 6 | 7 | 8 | class Main: 9 | def __init__(self, app): 10 | self.app = app 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def app(container): 15 | return container.dependency(kind='FACTORY', name='app')(App) 16 | 17 | 18 | @pytest.fixture(autouse=True) 19 | def main1(container): 20 | return container.dependency(name='main1')(Main) 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def main2(container): 25 | return container.dependency(name='main2')(Main) 26 | 27 | 28 | def test_creates_different_instances_for_each_instance_of_main(main1, main2): 29 | assert main1().app is not main2().app 30 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Vacuna 2 | site_author: Fernando Martínez González 3 | repo_url: https://github.com/frndmg/vacuna 4 | 5 | nav: 6 | - Home: README.md 7 | - Collaborate: COLLABORATE.md 8 | - Refs: refs.md 9 | 10 | theme: readthedocs 11 | 12 | plugins: 13 | - search 14 | - mkdocstrings: 15 | default_handler: python 16 | handlers: 17 | python: 18 | rendering: 19 | show_signature_annotations: True 20 | selection: 21 | filters: 22 | - "!^_" 23 | watch: 24 | - vacuna 25 | 26 | markdown_extensions: 27 | - toc: 28 | permalink: "#" 29 | baselevel: 3 30 | - pymdownx.snippets 31 | - admonition 32 | - pymdownx.details 33 | - pymdownx.superfences 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | skipsdist = true 4 | envlist = py37, py38, py39 5 | 6 | [testenv] 7 | whitelist_externals = poetry 8 | commands = 9 | poetry install -v 10 | poetry run pytest --doctest-modules --cov=vacuna 11 | 12 | [testenv:lint] 13 | whitelist_externals = poetry 14 | commands = 15 | poetry install -v 16 | poetry env info 17 | poetry run flake8 . 18 | poetry run isort . 19 | poetry run mkdocs build --strict 20 | 21 | [flake8] 22 | exclude = .tox,.venv,.git 23 | max-line-length = 80 24 | max-complexity = 16 25 | 26 | [pep8] 27 | max-line-length = 80 28 | 29 | [isort] 30 | line_length = 80 31 | case_sensitive = true 32 | use_parentheses = true 33 | include_trailing_comma = true 34 | multi_line_output = 3 35 | force_grid_wrap = 0 36 | 37 | [pytest] 38 | addopts = --doctest-modules --doctest-report ndiff --cov=vacuna 39 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Vacuna 2 | 3 | > Inject everything! 4 | 5 | ![PyPI](https://img.shields.io/pypi/v/vacuna) 6 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vacuna) 7 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/vacuna) 8 | ![PyPI - License](https://img.shields.io/pypi/l/vacuna) 9 | [![codecov](https://codecov.io/gh/frndmg/vacuna/branch/master/graph/badge.svg?token=L38OHXFKQO)](https://codecov.io/gh/frndmg/vacuna) 10 | 11 | Vacuna is a little library to provide dependency management for your python code. 12 | 13 | # Install 14 | 15 | ```bash 16 | pip install vacuna 17 | ``` 18 | 19 | # Usage 20 | 21 | ```python 22 | import vacuna 23 | 24 | container = vacuna.Container() 25 | 26 | @container.dependency(name='app') 27 | class App: 28 | def run(self): 29 | print('very important computation') 30 | 31 | @container.dependency() 32 | def main(app): 33 | app.run() 34 | 35 | if __name__ == '__main__': 36 | container.run(main) 37 | ``` -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package and Doc 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-python@v1 18 | 19 | - name: Install Poetry 20 | uses: snok/install-poetry@v1.1.7 21 | 22 | - name: Build and publish 23 | run: | 24 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 25 | poetry publish --build 26 | 27 | docs: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | with: 33 | fetch-depth: 0 34 | 35 | - uses: actions/setup-python@v1 36 | 37 | - name: Install Poetry 38 | uses: snok/install-poetry@v1.1.2 39 | 40 | - name: Install dependencies 41 | run: | 42 | poetry install -E docs --no-dev 43 | 44 | - name: Publish 45 | run: | 46 | poetry run mkdocs gh-deploy 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Fernando Martínez González 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. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "vacuna" 3 | version = "0.2.2" 4 | description = "Reusable Lightweight Pythonic Dependency Injection Library" 5 | authors = ["Fernando Martinez Gonzalez "] 6 | readme = "docs/README.md" 7 | license = "MIT" 8 | include = ["docs/**/*.md", "examples/**/*.py", "tests/**/*.py"] 9 | repository = "https://github.com/frndmg/vacuna" 10 | documentation = "https://vacuna.readthedocs.io/en/latest/" 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.7" 14 | typing-extensions = "^3.10.0" 15 | 16 | mkdocs = {version = "^1.2.2", optional = true} 17 | mkdocstrings = {version = "^0.15.2", optional = true} 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest-xdist = "^2.3.0" 21 | pytest-cov = "^2.12.1" 22 | pytest-clarity = "^1.0.1" 23 | pytest-sugar = "^0.9.4" 24 | pytest-benchmark = "^3.4.1" 25 | pytest-deadfixtures = "^2.2.1" 26 | isort = "^5.9.3" 27 | flake8 = "^3.9.2" 28 | autopep8 = "^1.5.7" 29 | mypy = "^0.910" 30 | tox = "^3.24.3" 31 | 32 | [tool.poetry.extras] 33 | docs = ["mkdocs", "mkdocstrings"] 34 | 35 | [build-system] 36 | requires = ["poetry-core>=1.0.0"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /examples/cli.py: -------------------------------------------------------------------------------- 1 | import vacuna 2 | 3 | container = vacuna.Container() 4 | 5 | 6 | @container.dependency(kind='SINGLETON') 7 | def config(args): 8 | return { 9 | 'path': args.path, 10 | } 11 | 12 | 13 | @container.dependency(kind='SINGLETON') 14 | def args(): 15 | from argparse import ArgumentParser 16 | 17 | parser = ArgumentParser() 18 | parser.add_argument( 19 | '--beauty', 20 | default=False, 21 | action='store_true', 22 | ) 23 | parser.add_argument('--path', default='.', type=str) 24 | 25 | return parser.parse_args() 26 | 27 | 28 | class App: 29 | def __init__(self, path: str, beauty: bool): 30 | self.path = path 31 | self.beauty = beauty 32 | 33 | def run(self): 34 | print( 35 | 'It is alive! ' 36 | f'path: `{self.path}`, ' 37 | f'beauty: `{self.beauty}`.' 38 | ) 39 | 40 | 41 | @container.dependency(kind='FACTORY') 42 | def app(path: str = config['path'], beauty: bool = args.beauty): 43 | return App(path, beauty) 44 | 45 | 46 | @container.dependency() 47 | def main(app: App): 48 | app.run() 49 | 50 | 51 | if __name__ == "__main__": 52 | container.run(main) 53 | -------------------------------------------------------------------------------- /tests/test_singleton.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class App: 5 | pass 6 | 7 | 8 | class Main: 9 | def __init__(self, app): 10 | self.app = app 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def app(container): 15 | return container.dependency(kind='SINGLETON', name='app')(App) 16 | 17 | 18 | @pytest.fixture(autouse=True) 19 | def main1(container): 20 | return container.dependency(name='main1')(Main) 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def main2(container): 25 | return container.dependency(name='main2')(Main) 26 | 27 | 28 | def test_singleton(main1, main2): 29 | assert main1().app is main2().app 30 | 31 | 32 | @pytest.fixture(autouse=True) 33 | def buz1and2(container): 34 | @container.dependency(kind='SINGLETON') 35 | def config(): 36 | return {'path': 'this is the path'} 37 | 38 | class Buz: 39 | def __init__(self, path=config['path']): 40 | self.path = path 41 | 42 | return ( 43 | container.dependency(name='buz1')(Buz), 44 | container.dependency(name='buz2')(Buz), 45 | ) 46 | 47 | 48 | def test_lazy_singleton(buz1and2): 49 | buz1, buz2 = buz1and2 50 | 51 | assert buz1().path is buz2().path 52 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | python-version: [3.7, 3.8, 3.9] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install Poetry 28 | uses: snok/install-poetry@v1.1.7 29 | with: 30 | virtualenvs-create: true 31 | virtualenvs-in-project: true 32 | 33 | - name: Load cached venv 34 | id: cached-poetry-dependencies 35 | uses: actions/cache@v2 36 | with: 37 | path: .venv 38 | key: venv-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} 39 | 40 | - name: Install dependencies 41 | run: poetry install --no-interaction --no-ansi 42 | 43 | - name: Lint 44 | run: | 45 | poetry run flake8 --count --show-source --statistics 46 | poetry run isort --check --diff . 47 | 48 | - name: Test 49 | run: | 50 | poetry run pytest --cov-report=xml 51 | 52 | - name: Upload coverage 53 | uses: codecov/codecov-action@v1 54 | with: 55 | file: ./coverage.xml 56 | if: ${{ always() }} 57 | -------------------------------------------------------------------------------- /vacuna/__init__.py: -------------------------------------------------------------------------------- 1 | """Small library to work with dependencies in Python""" 2 | 3 | import inspect 4 | from typing import Callable, Dict, Optional, Tuple 5 | 6 | from typing_extensions import Literal 7 | 8 | from .__version__ import __version__ 9 | from .lazy import Lazy, lazy, make_lazy, once 10 | 11 | Kind = Literal['factory', 'singleton', 'resource'] 12 | 13 | FACTORY = 'factory' # type: Kind 14 | SINGLETON = 'singleton' # type: Kind 15 | RESOURCE = 'resource' # type: Kind 16 | 17 | 18 | class Dependency: 19 | def __init__(self, name: str): 20 | self.name = name 21 | self.kind = FACTORY 22 | 23 | self.fn = None # type: Optional[Callable] 24 | self.args = None # type: Optional[Tuple[Lazy, ...]] 25 | self.kwargs = None # type: Optional[Dict[str, Lazy]] 26 | 27 | def validate(self): 28 | pass 29 | 30 | def set_values(self, fn: Callable, kind: Kind, *args: Lazy, **kwargs: Lazy): 31 | self.fn = lazy(fn) # TODO: do not use lazy here 32 | self.args = args 33 | self.kwargs = kwargs 34 | self.kind = kind 35 | 36 | self.lazy_fn = lambda: \ 37 | self.fn(*self.args, **self.kwargs) # type: ignore 38 | 39 | if self.kind == 'SINGLETON': 40 | self.lazy_fn = once(self.lazy_fn) 41 | 42 | def __call__(self): 43 | return self.lazy_fn()() 44 | 45 | 46 | class DependencyBuilder: 47 | def __init__(self, container: 'Container', kind: Kind = FACTORY, name=None): 48 | self.container = container 49 | self.kind = kind 50 | self.name = name 51 | 52 | def __call__(self, fn: Callable) -> Lazy: 53 | dependency = self.build_dependency(fn) 54 | 55 | return make_lazy(dependency, name=dependency.name) 56 | 57 | def build_dependency(self, fn) -> Dependency: 58 | name = self.get_name(fn) 59 | 60 | dependency = self.container.get_dependency(name) 61 | args, kwargs = self.get_dependencies(fn) 62 | 63 | dependency.set_values(fn, self.kind, *args, **kwargs) 64 | 65 | for arg in args: 66 | if isinstance(arg, Dependency): 67 | self.container.update_dependency(arg) 68 | 69 | for arg in kwargs.values(): 70 | if isinstance(arg, Dependency): 71 | self.container.update_dependency(arg) 72 | 73 | self.container.update_dependency(dependency) 74 | 75 | return dependency 76 | 77 | def get_name(self, fn): 78 | if self.name is not None: 79 | return self.name 80 | return fn.__name__ 81 | 82 | def get_dependencies(self, fn): 83 | signature = inspect.signature(fn) 84 | 85 | dependencies = [] 86 | 87 | for parameter in signature.parameters.values(): 88 | name = parameter.name 89 | default = parameter.default 90 | 91 | if default is not inspect._empty: 92 | dependencies.append(default) 93 | else: 94 | dependency = self.container.get_dependency(name) 95 | dependencies.append(dependency) 96 | 97 | return dependencies, {} 98 | 99 | 100 | class Container: 101 | def __init__(self): 102 | self._dependencies = {} # type: Dict[str, Dependency] 103 | 104 | def dependency(self, kind: Kind = FACTORY, name=None) -> DependencyBuilder: 105 | return DependencyBuilder(self, kind=kind, name=name) 106 | 107 | def run(self, dependency: Dependency): 108 | dependency() 109 | 110 | def get_dependency(self, name: str) -> Dependency: 111 | return self._dependencies.get(name, Dependency(name)) 112 | 113 | def update_dependency(self, dependency: Dependency): 114 | self._dependencies[dependency.name] = dependency 115 | 116 | 117 | __all__ = ['__version__', 'Container'] 118 | -------------------------------------------------------------------------------- /vacuna/lazy.py: -------------------------------------------------------------------------------- 1 | """A lazy object is a deferred computation, `lambda: 2+2` is a deferred 2 | computation for example, nothing will be called until we execute""" 3 | 4 | 5 | from abc import ABC, abstractmethod 6 | from functools import wraps 7 | from operator import add, and_, attrgetter, itemgetter, mul, or_, sub 8 | 9 | 10 | class Lazy(ABC): 11 | """Lazy object representation 12 | 13 | Convert a deferred computation into a lazy object using `make_lazy`. 14 | 15 | Examples: 16 | 17 | ```python 18 | >>> isinstance(make_lazy(lambda: 1), Lazy) 19 | True 20 | >>> isinstance(lambda: 1, Lazy) 21 | True 22 | 23 | ``` 24 | 25 | ```python 26 | >>> x = make_lazy(lambda: 1) 27 | >>> y = lambda: 2 28 | >>> z = x + y 29 | >>> z() 30 | 3 31 | 32 | ``` 33 | """ 34 | 35 | def __getattr__(self, attr) -> 'Lazy': 36 | return _make_lazy( 37 | once(_map_lazy(attrgetter(attr), self)), 38 | name=f'{self}.{attr}', 39 | ) 40 | 41 | def __getitem__(self, item) -> 'Lazy': 42 | return _make_lazy( 43 | once(_map_lazy(itemgetter(item), self)), 44 | name=f'{self}[{repr(item)}]', 45 | ) 46 | 47 | def __add__(self, other): 48 | return _make_lazy( 49 | once(_lazy_add(self, other)), 50 | name=f'({self} + {other})', 51 | ) 52 | 53 | def __repr__(self) -> str: 54 | raise NotImplementedError 55 | 56 | @abstractmethod 57 | def __call__(self): 58 | raise NotImplementedError 59 | 60 | @classmethod 61 | def __subclasshook__(cls, C): 62 | if cls is Lazy: 63 | if any('__call__' in B.__dict__ for B in C.__mro__): 64 | return True 65 | return NotImplemented 66 | 67 | 68 | class LazyError(Exception): 69 | pass 70 | 71 | 72 | def lazy(f): 73 | """Creates a lazy callable from a function 74 | 75 | Examples: 76 | 77 | 78 | ```python 79 | >>> from collections import namedtuple 80 | >>> X = lazy(namedtuple('X', 'a b')) 81 | >>> a = lambda: 1 82 | >>> b = lambda: 'foo' 83 | 84 | ``` 85 | 86 | The reference is the same after constructing the object 87 | 88 | ```python 89 | >>> c = X(a, b=b) 90 | >>> c() is c() 91 | True 92 | 93 | ``` 94 | 95 | ```python 96 | >>> c1 = X(a, b=b) 97 | >>> c2 = X(a, b=b) 98 | >>> c1() is c2() 99 | False 100 | 101 | ``` 102 | 103 | You can also use non lazy objects 104 | 105 | ```python 106 | >>> c = X(a, b=2) 107 | >>> c() 108 | X(a=1, b=2) 109 | 110 | ``` 111 | 112 | Be careful with mutations 113 | 114 | ```python 115 | >>> c = X(a=make_lazy(lambda: [1]), b=2) 116 | >>> d = c() 117 | >>> d.a[0] = 3 118 | >>> c() 119 | X(a=[3], b=2) 120 | 121 | ``` 122 | """ 123 | @wraps(f) 124 | def _lazy(*args, **kwargs): 125 | return make_lazy( 126 | lambda: f( 127 | *map_list(args, call_if_lazy), 128 | **map_dict(kwargs, call_if_lazy) 129 | ), 130 | name=f.__name__, 131 | ) 132 | 133 | return _lazy 134 | 135 | 136 | def make_lazy(lazy_obj, name=''): 137 | """Creates a Lazy object that lets you inspect the properties of the object 138 | in a lazy fashion. 139 | 140 | Examples: 141 | 142 | ```python 143 | >>> x = make_lazy(lambda: {'a': [{'b': True}]}) 144 | >>> y = x['a'][0]['b'] 145 | >>> y() 146 | True 147 | 148 | ``` 149 | 150 | ```python 151 | >>> from collections import namedtuple 152 | >>> X = namedtuple('X', 'a b') 153 | >>> x = make_lazy(lambda: X(a=[{'b': True}], b=False)) 154 | >>> y = x.a[0]['b'] 155 | >>> y() 156 | True 157 | 158 | ``` 159 | 160 | ```python 161 | >>> y = x.a[1]['b'] # this line does not break 162 | >>> y() 163 | Traceback (most recent call last): 164 | ... 165 | vacuna.lazy.LazyError: error when evaluating `.a[1]` 166 | 167 | ``` 168 | 169 | ```python 170 | >>> x = make_lazy(lambda: 2) 171 | >>> y = make_lazy(lambda: 1) 172 | >>> z = x + y 173 | >>> z() 174 | 3 175 | 176 | ``` 177 | 178 | ```python 179 | >>> x = make_lazy(lambda: []) 180 | >>> x() 181 | [] 182 | >>> x() is x() 183 | True 184 | 185 | ``` 186 | """ 187 | return _make_lazy(once(lazy_obj), name) 188 | 189 | 190 | def _make_lazy(lazy_obj, name=''): 191 | class _Lazy(Lazy): 192 | def __repr__(self): 193 | return name 194 | 195 | def __call__(self): 196 | try: 197 | return lazy_obj() 198 | except LazyError: 199 | raise 200 | except Exception as e: 201 | raise LazyError(f'error when evaluating `{name}`') from e 202 | 203 | return _Lazy() 204 | 205 | 206 | def once(f): 207 | """Execute the given function only once and cache the result 208 | 209 | Examples: 210 | 211 | Without `once` 212 | 213 | ```python 214 | >>> def f(x=[]): 215 | ... x.append(1) 216 | ... return x 217 | >>> f() 218 | [1] 219 | >>> f() 220 | [1, 1] 221 | 222 | ``` 223 | 224 | With `once` 225 | 226 | ```python 227 | >>> @once 228 | ... def f(x=[]): 229 | ... x.append(1) 230 | ... return x 231 | >>> f() 232 | [1] 233 | >>> f() 234 | [1] 235 | 236 | ``` 237 | 238 | """ 239 | 240 | x = None 241 | executed = False 242 | 243 | def _once(): 244 | nonlocal x, executed 245 | 246 | if not executed: 247 | x = f() 248 | executed = True 249 | 250 | return x 251 | 252 | return _once 253 | 254 | 255 | def map_list(xs, f): 256 | return tuple(f(x) for x in xs) 257 | 258 | 259 | def map_dict(d, f): 260 | return dict((k, f(v)) for k, v in d.items()) 261 | 262 | 263 | def call_if_lazy(x): 264 | if isinstance(x, Lazy): 265 | return x() 266 | return x 267 | 268 | 269 | def _map_lazy(f, lazy_x): 270 | return lambda: f(call_if_lazy(lazy_x)) 271 | 272 | 273 | def _lazy_add(x, y): 274 | return lambda: add(call_if_lazy(x), call_if_lazy(y)) 275 | 276 | 277 | def _lazy_sub(x, y): 278 | return lambda: sub(call_if_lazy(x), call_if_lazy(y)) 279 | 280 | 281 | def _lazy_mul(x, y): 282 | return lambda: mul(call_if_lazy(x), call_if_lazy(y)) 283 | 284 | 285 | def _lazy_and_(x, y): 286 | return lambda: and_(call_if_lazy(x), call_if_lazy(y)) 287 | 288 | 289 | def _lazy_or_(x, y): 290 | return lambda: or_(call_if_lazy(x), call_if_lazy(y)) 291 | --------------------------------------------------------------------------------