Skip to content

Commit f9cb6c1

Browse files
committed
First commit
- Basic implementation. - Package structure, dev tools, licensing, tests.
0 parents  commit f9cb6c1

10 files changed

Lines changed: 307 additions & 0 deletions

File tree

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
__pycache__/
5+
*.pyc
6+
*.pyo
7+
*.pyd
8+
.mypy_cache/
9+
.pytest_cache/
10+
.coverage
11+
*.egg-info/
12+
dist/

LICENSES/ISC.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
ISC License
2+
3+
Copyright (c) 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
4+
5+
Permission to use, copy, modify, and/or distribute this software for any
6+
purpose with or without fee is hereby granted, provided that the above
7+
copyright notice and this permission notice appear in all copies.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10+
WITH RESPECT TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Makefile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
.PHONY: all format lint test coverage clean
5+
6+
all: format lint coverage
7+
8+
format:
9+
black .
10+
isort .
11+
12+
lint:
13+
reuse lint
14+
flake8 .
15+
mypy .
16+
17+
test:
18+
pytest -q
19+
20+
coverage:
21+
pytest --cov=apywire --cov-report=term-missing --cov-fail-under=95
22+
23+
clean:
24+
find . -name __pycache__ -type d -exec rm -rf {} +
25+
rm -f *.pyc *.pyo *.pyd .coverage
26+
rm -rf .mypy_cache .pytest_cache *.egg-info dist

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com> -->
2+
<!-- SPDX-License-Identifier: ISC -->
3+
4+
# apywire
5+
6+
A Python package to wire up objects.
7+
8+
---
9+
10+
Under development and unstable, API might change at any moment.

REUSE.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version = 1
2+
3+
SPDX-PackageName = "apywire"
4+
SPDX-PackageSupplier = "Alexandre Gomes Gaigalas <alganet@gmail.com>"
5+
SPDX-PackageDownloadLocation = "https://github.com/alganet/apywire"
6+
7+
[[annotations]]
8+
path = "**/__pycache__/**"
9+
precedence = "aggregate"
10+
SPDX-FileCopyrightText = "2025 Alexandre Gomes Gaigalas <alganet@gmail.com>"
11+
SPDX-License-Identifier = "ISC"

apywire/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
"""A package to wire up objects."""
5+
6+
from .wiring import (
7+
Blueprint,
8+
InstanceData,
9+
Spec,
10+
Wired,
11+
compile,
12+
wire,
13+
)
14+
15+
__all__ = [
16+
"Blueprint",
17+
"InstanceData",
18+
"Spec",
19+
"Wired",
20+
"compile",
21+
"wire",
22+
]

apywire/wiring.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
"""Core wiring functionality."""
5+
6+
import importlib
7+
from typing import Dict, List, Tuple, TypeAlias, TypeVar, cast
8+
9+
T = TypeVar("T")
10+
11+
InstanceData: TypeAlias = Dict[str, object]
12+
Blueprint: TypeAlias = Tuple[str, str, str, InstanceData]
13+
14+
Spec: TypeAlias = Dict[str, InstanceData]
15+
16+
17+
def _parse_spec[T](spec: Spec) -> List[Blueprint]:
18+
"""Parses a Spec into Blueprints for instantiation."""
19+
items = []
20+
for key, value in spec.items():
21+
type_str, name = key.rsplit(" ", 1)
22+
parts = type_str.split(".")
23+
module_name = ".".join(parts[:-1])
24+
class_name = parts[-1]
25+
items.append((module_name, class_name, name, value))
26+
return items
27+
28+
29+
class Wired[T]:
30+
"""Lazy-loaded container for wired objects."""
31+
32+
def __init__(self, parsed: List[Blueprint]) -> None:
33+
self._parsed = {
34+
name: (module_name, class_name, value)
35+
for module_name, class_name, name, value in parsed
36+
}
37+
self._instances: Dict[str, object] = {}
38+
39+
def __getattr__(self, name: str) -> object:
40+
"""Get the instantiated object for name via attribute access."""
41+
if name not in self._parsed:
42+
raise AttributeError(f"no attribute '{name}'")
43+
if name not in self._instances:
44+
module_name, class_name, value = self._parsed[name]
45+
module = importlib.import_module(module_name)
46+
cls = cast(type[T], getattr(module, class_name))
47+
self._instances[name] = cls(**value)
48+
return self._instances[name]
49+
50+
51+
def wire[T](spec: Spec) -> Wired[T]:
52+
"""Creates a lazy-loaded Wired object from a Spec."""
53+
return Wired(_parse_spec(spec))
54+
55+
56+
def compile[T](spec: Spec) -> str:
57+
"""Compiles a Spec into a string containing Python code."""
58+
parsed = _parse_spec(spec)
59+
modules = set()
60+
properties = []
61+
for module_name, class_name, name, value in parsed:
62+
modules.add(module_name)
63+
type_str = f"{module_name}.{class_name}"
64+
if isinstance(value, dict):
65+
kwargs = ", ".join(f"{k}={repr(v)}" for k, v in value.items())
66+
prop = (
67+
" @property\n"
68+
f" def {name}(self):\n"
69+
f" return {type_str}({kwargs})"
70+
)
71+
properties.append(prop)
72+
imports = [f"import {module}" for module in sorted(modules)]
73+
class_def = ["class Compiled:"] + properties + ["compiled = Compiled()"]
74+
return "\n".join(imports + class_def)

pyproject.toml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
[build-system]
5+
requires = ["setuptools", "wheel"]
6+
build-backend = "setuptools.build_meta"
7+
8+
[project]
9+
name = "apywire"
10+
version = "0.1.0"
11+
description = "A package to wire up objects"
12+
readme = "README.md"
13+
license = {text = "ISC"}
14+
authors = [{name = "Alexandre Gomes Gaigalas", email = "alganet@gmail.com"}]
15+
maintainers = [{name = "Alexandre Gomes Gaigalas", email = "alganet@gmail.com"}]
16+
keywords = ["object", "wiring", "dependency", "injection"]
17+
classifiers = [
18+
"Development Status :: 3 - Alpha",
19+
"Intended Audience :: Developers",
20+
"License :: OSI Approved :: ISC License (ISCL)",
21+
"Programming Language :: Python :: 3",
22+
"Topic :: Software Development :: Libraries :: Python Modules",
23+
]
24+
dependencies = []
25+
requires-python = ">=3.10"
26+
27+
[project.urls]
28+
Homepage = "https://github.com/alganet/apywire"
29+
Repository = "https://github.com/alganet/apywire"
30+
Issues = "https://github.com/alganet/apywire/issues"
31+
32+
[project.optional-dependencies]
33+
dev = [
34+
"black",
35+
"isort",
36+
"flake8",
37+
"mypy",
38+
"pytest",
39+
"reuse",
40+
"coverage",
41+
]
42+
43+
[tool.setuptools.packages.find]
44+
where = ["."]
45+
46+
[tool.black]
47+
line-length = 79
48+
49+
[tool.isort]
50+
profile = "black"
51+
line_length = 79
52+
53+
[tool.flake8]
54+
max-line-length = 79
55+
56+
[tool.mypy]
57+
mypy_path = "."
58+
strict = true
59+
disallow_any_unimported = true
60+
disallow_any_decorated = true
61+
disallow_any_explicit = true
62+
disallow_subclassing_any = true
63+
disallow_any_expr = true
64+
warn_return_any = true
65+
warn_unused_configs = true
66+
67+
[tool.pytest.ini_options]
68+
testpaths = ["tests"]

tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
"""Tests for apywire."""

tests/test_simple.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: 2025 Alexandre Gomes Gaigalas <alganet@gmail.com>
2+
# SPDX-License-Identifier: ISC
3+
4+
import datetime
5+
from typing import Protocol
6+
7+
import apywire
8+
9+
10+
def test_simple_load_constructor_args() -> None:
11+
spec: apywire.Spec = {
12+
"datetime.datetime yearsAgo": {
13+
"day": 13,
14+
"month": 12,
15+
"year": 2003,
16+
}
17+
}
18+
wired: apywire.Wired[object] = apywire.wire(spec)
19+
instance = wired.yearsAgo
20+
assert isinstance(instance, datetime.datetime)
21+
assert instance.year == 2003
22+
assert instance.month == 12
23+
assert instance.day == 13
24+
25+
26+
def test_simple_raise_on_nonexistent_wired_attribute() -> None:
27+
try:
28+
apywire.wire({}).nonexistent
29+
assert False, "Should have raised AttributeError"
30+
except AttributeError as e:
31+
assert "no attribute 'nonexistent'" in str(e)
32+
33+
34+
def test_simple_compile_constructor_args() -> None:
35+
spec: apywire.Spec = {
36+
"datetime.datetime birthday": {
37+
"day": 25,
38+
"month": 12,
39+
"year": 1990,
40+
}
41+
}
42+
43+
pythonCode = apywire.compile(spec)
44+
assert (
45+
"""import datetime
46+
class Compiled:
47+
@property
48+
def birthday(self):
49+
return datetime.datetime(day=25, month=12, year=1990)
50+
compiled = Compiled()"""
51+
in pythonCode
52+
)
53+
54+
class MockHasBirthday(Protocol):
55+
birthday: datetime.datetime
56+
57+
execd: dict[str, MockHasBirthday] = {}
58+
exec(pythonCode, execd)
59+
compiled = execd["compiled"]
60+
61+
instance = compiled.birthday
62+
assert isinstance(instance, datetime.datetime)
63+
assert instance.year == 1990
64+
assert instance.month == 12
65+
assert instance.day == 25

0 commit comments

Comments
 (0)