Skip to content

Commit fb44c7f

Browse files
development up to tests
0 parents  commit fb44c7f

File tree

18 files changed

+535
-0
lines changed

18 files changed

+535
-0
lines changed

.gitignore

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Virtual environments
2+
.venv/
3+
venv/
4+
ENV/
5+
env/
6+
7+
# Python cache and bytecode
8+
__pycache__/
9+
*.py[cod]
10+
*$py.class
11+
12+
# Distribution / packaging
13+
build/
14+
dist/
15+
*.egg-info/
16+
*.egg
17+
pip-wheel-metadata/
18+
.eggs/
19+
*.egg-link
20+
21+
# Installer logs
22+
pip-log.txt
23+
pip-delete-this-directory.txt
24+
25+
# pytest
26+
.pytest_cache/
27+
28+
# Coverage
29+
.coverage
30+
.coverage.*
31+
htmlcov/
32+
coverage.xml
33+
34+
# mypy / cache
35+
.mypy_cache/
36+
.pyre/
37+
38+
# IDEs and editors
39+
.vscode/
40+
.idea/
41+
*.sublime-project
42+
*.sublime-workspace
43+
44+
# OS files
45+
.DS_Store
46+
Thumbs.db
47+
48+
# Jupyter
49+
.ipynb_checkpoints/
50+
51+
# Static analysis / tooling
52+
.cache/
53+
.mercurial/
54+
55+
# Logs
56+
*.log
57+
58+
# Misc
59+
*.sqlite3
60+
*.db
61+
62+
# Local env files
63+
.env
64+
.env.local
65+
.env.*.local
66+
67+
# Node modules (if any experiments use node tooling)
68+
node_modules/
69+
70+
# Generated docs
71+
docs/_build/
72+
73+
# Ignore build artifacts created by editable installs
74+
*.egg-link
75+
76+
# Keep track of any project-specific ignores below
77+
# e.g. experiments/data/, notebooks/data/, etc.

.pre-commit-config.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
repos:
2+
- repo: https://github.com/psf/black
3+
rev: 24.8.0
4+
hooks:
5+
- id: black
6+
args: ["--line-length=88"]
7+
8+
- repo: https://github.com/astral-sh/ruff-pre-commit
9+
rev: v0.6.2
10+
hooks:
11+
- id: ruff
12+
args: ["--fix"]
13+
14+
- repo: https://github.com/pre-commit/mirrors-mypy
15+
rev: v1.11.2
16+
hooks:
17+
- id: mypy
18+
additional_dependencies: ["types-setuptools"]
19+
20+
- repo: https://github.com/pre-commit/pre-commit-hooks
21+
rev: v4.6.0
22+
hooks:
23+
- id: trailing-whitespace
24+
- id: end-of-file-fixer
25+
- id: check-yaml
26+
- id: check-added-large-files

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
All notable changes to this project are recorded here.
3+
4+
## [Unreleased]
5+
6+
### Added
7+
- Mini calculator library (`example_lib`) with:
8+
- `Operation`, `OperationRegistry`, and `Calculator` classes
9+
- example operations: `add`, `sub`, `mul`, `div`, `neg`, `sqr`
10+
- Minimal test suite (pytest) with unit and edge tests
11+
- `pyproject.toml` for editable installs and packaging
12+
- `.gitignore` and basic repo scaffolding (`src/`, `tests/`, `experiments/`, `notebooks/`, `docs/`, `scripts/`)
13+
14+
### Changed
15+
- Replaced decorator-based registration with explicit `Operation` registration API
16+
17+
### Fixed
18+
- Import path handling in tests: tests now assume editable install; `conftest.py` removed sys.path hacks
19+
20+
---
21+
22+
## [0.1.0] - 2025-10-10
23+
24+
Initial public release
25+
26+
### Added
27+
- Project scaffold and minimal working library to demonstrate portfolio-ready features

docs/.gitkeep

Whitespace-only changes.

notebooks/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[build-system]
2+
requires = ["setuptools>=61", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "example-lib"
7+
version = "0.1.0"
8+
description = "Mini calculator library"
9+
readme = "README.md"
10+
requires-python = ">=3.8"
11+
authors = [{ name = "Andrea" }]
12+
license = { text = "MIT" }
13+
dependencies = []
14+
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"License :: OSI Approved :: MIT License",
18+
"Operating System :: OS Independent",
19+
]
20+
21+
[project.optional-dependencies]
22+
dev = ["pytest", "pytest-cov"]
23+
24+
[tool.setuptools]
25+
package-dir = {"" = "src"}
26+
27+
[tool.setuptools.packages.find]
28+
where = ["src"]
29+
include = ["example_lib*"]

scripts/.gitkeep

Whitespace-only changes.

src/example_lib/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""example_lib: mini math utility library
2+
3+
Exports:
4+
- Operation, Operation registry, Calculator, convenience operations
5+
"""
6+
7+
from .exceptions import CalculatorError, OperationError, RegistryError
8+
from .operations import Operation, ADD, SUB, MUL, DIV, NEG, SQR
9+
from .registry import OperationRegistry
10+
from .calculator import Calculator
11+
from .utils import is_number
12+
13+
__all__ = [
14+
"CalculatorError",
15+
"OperationError",
16+
"RegistryError",
17+
"Operation",
18+
"OperationRegistry",
19+
"Calculator",
20+
"ADD",
21+
"SUB",
22+
"MUL",
23+
"DIV",
24+
"NEG",
25+
"SQR",
26+
"is_number",
27+
]
28+
29+
# Provide a default registry populated with common ops
30+
_default_registry = OperationRegistry()
31+
for op in (ADD, SUB, MUL, DIV, NEG, SQR):
32+
_default_registry.register(op)
33+
34+
35+
# Convenience constructor using default registry
36+
def default_calculator() -> Calculator:
37+
return Calculator(registry=_default_registry)

src/example_lib/calculator.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Calculator that can register and compose operations."""
2+
3+
from typing import Callable, Iterable, List, Any, Optional
4+
5+
from .registry import OperationRegistry
6+
from .operations import Operation
7+
from .exceptions import OperationError
8+
9+
10+
class Calculator:
11+
"""A tiny calculator that uses an OperationRegistry.
12+
13+
Features:
14+
- register operations (via registry or helper)
15+
- apply a named operation to arguments
16+
- compose a sequence of operations into a single callable
17+
"""
18+
19+
def __init__(self, registry: Optional[OperationRegistry] = None):
20+
self.registry = registry or OperationRegistry()
21+
22+
def register(self, op: Operation, *, replace: bool = False) -> None:
23+
self.registry.register(op, replace=replace)
24+
25+
def apply(self, op_name: str, *args) -> Any:
26+
op = self.registry.get(op_name)
27+
try:
28+
return op(*args)
29+
except Exception as exc:
30+
raise OperationError(
31+
f"Error applying operation '{op_name}': {exc}"
32+
) from exc
33+
34+
def compose(
35+
self, ops: Iterable[str], *, left_to_right: bool = True
36+
) -> Callable[[Any], Any]:
37+
"""Compose a sequence of unary operations into a single callable.
38+
39+
The composed function takes one argument and applies the operations in order.
40+
Only unary operations (arity == 1) are supported for composition.
41+
42+
Args:
43+
ops: iterable of operation names
44+
left_to_right: if True, apply first op then next (f2(f1(x))).
45+
46+
Returns:
47+
callable f(x)
48+
"""
49+
50+
op_list: List[Operation] = [self.registry.get(name) for name in ops]
51+
for op in op_list:
52+
if op.arity != 1:
53+
raise OperationError(f"Cannot compose non-unary operation: {op.name}")
54+
55+
if left_to_right:
56+
57+
def composed(x):
58+
val = x
59+
for op in op_list:
60+
val = op(val)
61+
return val
62+
63+
else:
64+
65+
def composed(x):
66+
val = x
67+
for op in reversed(op_list):
68+
val = op(val)
69+
return val
70+
71+
return composed
72+
73+
def chain(self, sequence: Iterable[str], initial: Any) -> Any:
74+
"""Apply a mixed sequence of operations to an initial value.
75+
76+
For binary operations, the operation consumes (current_value, next_input)
77+
and returns a new current_value. To support binary ops in a chain, the
78+
sequence should alternate between operation names and provided literals
79+
(which are interpreted as inputs). Example:
80+
81+
sequence = ['add', 5, 'sqr']
82+
chain(sequence, initial=2) -> sqr(add(2,5)) = (2+5)^2
83+
84+
This is a very small DSL useful for demos.
85+
"""
86+
seq = list(sequence)
87+
cur = initial
88+
i = 0
89+
while i < len(seq):
90+
item = seq[i]
91+
if isinstance(item, str):
92+
op = self.registry.get(item)
93+
if op.arity == 1:
94+
cur = op(cur)
95+
i += 1
96+
elif op.arity == 2:
97+
# expect next item as argument
98+
if i + 1 >= len(seq):
99+
raise OperationError(
100+
f"Operation '{op.name}' expects an additional argument in the sequence"
101+
)
102+
arg = seq[i + 1]
103+
cur = op(cur, arg)
104+
i += 2
105+
else:
106+
raise OperationError("Only arity 1 or 2 supported in chain")
107+
else:
108+
# literal encountered: treat as updating current value
109+
cur = item
110+
i += 1
111+
return cur

src/example_lib/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Custom exceptions for the mini calculator library."""
2+
3+
4+
class CalculatorError(Exception):
5+
"""Base class for calculator-related errors."""
6+
7+
8+
class OperationError(CalculatorError):
9+
"""Raised when an operation fails (e.g. wrong arity or invalid input)."""
10+
11+
12+
class RegistryError(CalculatorError):
13+
"""Raised for registry problems (duplicate name, not found)."""

0 commit comments

Comments
 (0)