Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
python -m pip install --upgrade pip
pip install tox mdformat mdformat-gfm
- name: MDFormat check
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md docs/tooling_evaluation.md
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md
continue-on-error: true
- name: Lint with tox
run: tox -e lint
Expand Down
7 changes: 7 additions & 0 deletions fix_is_zero_arg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

def modify_core(filepath):
with open(filepath, 'r') as f:
content = f.read()

# We will just use replace_with_git_merge_diff below.
16 changes: 15 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ build-backend = "setuptools.build_meta"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py312, py313, lint, type
envlist = py312, py313, pyodide, micropython, lint, type

[testenv]
# install pytest in the virtualenv where commands will be executed
Expand All @@ -48,6 +48,19 @@ commands =
# NOTE: you can run any command line tool here - not just tests
pytest --cov=src --cov-append --cov-report=term-missing --cov-branch

[testenv:pyodide]
extras = test
deps =
pytest-pyodide
commands =
pytest test/ --run-in-pyodide

[testenv:micropython]
skip_install = true
allowlist_externals = micropython
commands =
micropython run_micropython_tests.py

[testenv:report]
deps = coverage
skip_install = true
Expand Down Expand Up @@ -97,6 +110,7 @@ convention = "google"
python_version = "3.12"
warn_return_any = false
warn_unused_configs = true
disable_error_code = ["misc"]

[tool.setuptools.packages.find]
where = ["src"]
Expand Down
9 changes: 9 additions & 0 deletions run_micropython_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
import os

# Ensure src is in the path
sys.path.append(os.getcwd() + '/src')
sys.path.append(os.getcwd() + '/test')

import test_micropython_core
test_micropython_core.run_all_tests()
174 changes: 133 additions & 41 deletions src/closure_collector/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,74 @@
import inspect
from abc import ABCMeta, abstractmethod
from collections.abc import Iterable, Mapping
from itertools import chain
from pprint import pformat
try:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jules are there cases where only some of these are available or can we combine all of these into one try/except?

from typing import Any
except ImportError: # MicroPython compatibility fallback for missing typing
Any = object # type: ignore[assignment,misc]

from closure_collector.util import ClosureCollectorException, is_rule, rebind
try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

try:
from collections.abc import Callable
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Callable = object # type: ignore[assignment,misc]

try:
from typing import TypeVar
except ImportError: # MicroPython compatibility fallback for missing typing

def TypeVar(name: str, bound: Any = Any) -> Any: # type: ignore[misc,no-redef]
return object


try:
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
except TypeError:
_FuncT = object # type: ignore[assignment,misc]

try:
from abc import ABCMeta, abstractmethod
except ImportError: # MicroPython compatibility fallback for missing abc

class ABCMeta(type): # type: ignore[no-redef]
pass

def abstractmethod(funcobj: _FuncT) -> _FuncT: # noqa: UP047
return funcobj # type: ignore[misc]


try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Iterable = object # type: ignore[assignment,misc]
Mapping = object # type: ignore[assignment,misc]

try:
from itertools import chain
except ImportError: # MicroPython compatibility fallback for missing itertools

class chain: # type: ignore[no-redef]
def __init__(self, *iterables: Any):
self.iterables = iterables

def __iter__(self) -> Any:
for it in self.iterables:
yield from it

@classmethod
def from_iterable(cls, iterables: Any) -> Any:
return cls(*iterables)


try:
from pprint import pformat
except ImportError: # MicroPython compatibility fallback for missing pprint
pformat = repr # type: ignore[assignment]

from closure_collector.util import ClosureCollectorException, get_cell_contents, is_rule, is_zero_arg, rebind # noqa: E402

CLOSURE_ATTRS = {"root", "cache", "peers", "promises"}

Expand All @@ -19,46 +83,73 @@ def __str__(self):
return pformat(self.__dict__)


class CCBase(metaclass=ABCMeta):
"""Base class for Closure Collector Objects of all sorts"""
if hasattr(ABCMeta, "__new__"):

@abstractmethod
def check(self, path):
"""
check for any contents that would prevent this Aggregator from being used normally, esp sheared.
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
class CCBase(metaclass=ABCMeta):
"""Base class for Closure Collector Objects of all sorts"""

@abstractmethod
def shear(self, record_errors=False):
"""
Convert this closure collection into a simple object
@abstractmethod
def check(self, path):
"""
check for any contents that would prevent this Aggregator from being used normally, esp sheared.
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
pass

:param record_errors: if True any exception raised will be stored in place of the result that caused it rather
than continuing up the call stack
@abstractmethod
def shear(self, record_errors=False):
"""
Convert this closure collection into a simple object

:return: a simple object representing these closures
"""
:param record_errors: if True any exception raised will be stored in place of the result that caused it rather
than continuing up the call stack

@abstractmethod
def __dir__(self):
"""Closure collector objects all support the dir() method returning the added attributes"""
pass
:return: a simple object representing these closures
"""
pass

def __call__(self):
"""
Call must be specified so that Closure Collections can be nested within eachother
@abstractmethod
def __dir__(self):
"""Closure collector objects all support the dir() method returning the added attributes"""
pass

:return: self
"""
return self
def __call__(self):
"""
Call must be specified so that Closure Collections can be nested within eachother

def clear_cache(self):
"""Empty any cache kept on this object"""
:return: self
"""
return self

def get_relatives(self) -> Iterable:
return ()
def clear_cache(self):
"""Empty any cache kept on this object"""
pass

def get_relatives(self) -> Iterable:
return ()
else:

class CCBase: # type: ignore[no-redef]
"""Base class for Closure Collector Objects of all sorts"""

def check(self, path):
pass

def shear(self, record_errors=False):
pass

def __dir__(self):
return []

def __call__(self):
return self

def clear_cache(self):
pass

def get_relatives(self) -> Iterable:
return ()


class DynamicClosureCollector(CCBase):
Expand Down Expand Up @@ -147,17 +238,18 @@ def __bool__(self):
return bool(self.promises)

def make_callable(self, value):
if callable(value) and len(inspect.signature(value).parameters) == 0:
if is_zero_arg(value):
ret = value
if isinstance(value, DynamicClosureCollector):
value.peers.add(self)
if value.root is None:
if getattr(value, "root", None) is None:
value.root = self
# if it's a closure and there is something in there
if hasattr(value, "__closure__") and value.__closure__:
for closure in value.__closure__:
if isinstance(closure.cell_contents, DynamicClosureCollector):
closure.cell_contents.peers.add(self)
contents = get_cell_contents(closure)
if isinstance(contents, DynamicClosureCollector):
contents.peers.add(self)
else:
ret = lambda: value
return ret
Expand Down
59 changes: 54 additions & 5 deletions src/closure_collector/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
from numbers import Number
from types import FunctionType
try:
from numbers import Number
except ImportError: # MicroPython compatibility fallback for missing numbers
Number = (int, float, complex) # type: ignore[assignment,misc]

try:
from typing import Any
except ImportError: # MicroPython compatibility fallback for missing typing
Any = object # type: ignore[assignment,misc]

try:
from types import FunctionType
except ImportError: # MicroPython compatibility fallback for missing types
FunctionType = type(lambda: None) # type: ignore[assignment,misc]

try:
import inspect

def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
return len(inspect.signature(value).parameters) == 0

def get_cell_contents(cell: Any) -> Any:
return cell.cell_contents

def set_cell_contents(cell: Any, value: Any) -> None:
cell.cell_contents = value

except ImportError: # MicroPython compatibility fallback for missing inspect

def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
try:
return value.__code__.co_argcount == 0
except AttributeError:
return True

def get_cell_contents(cell: Any) -> Any:
try:
return cell.cell_contents
except AttributeError:
return cell

def set_cell_contents(cell: Any, value: Any) -> None:
try:
cell.cell_contents = value
except AttributeError:
pass # Read-only fallback in MicroPython


class ClosureCollectorException(AttributeError):
Expand All @@ -9,8 +57,8 @@ class ClosureCollectorException(AttributeError):
def rebind(callable, from_obj, to_obj):
if getattr(callable, "__closure__", False):
for cell in callable.__closure__:
if cell.cell_contents is from_obj:
cell.cell_contents = to_obj
if get_cell_contents(cell) is from_obj:
set_cell_contents(cell, to_obj)


def is_rule(func):
Expand All @@ -19,7 +67,8 @@ def is_rule(func):

if getattr(func, "__closure__", False): ## TODO replace with inspect_getclosurevars, probably inspect only nonlocals
for cell in func.__closure__:
if not isinstance(cell.cell_contents, (str, Number, bytes, tuple, frozenset)):
contents = get_cell_contents(cell)
if not isinstance(contents, (str, Number, bytes, tuple, frozenset)):
return True
try:
if set(func.__globals__).intersection(func.__code__.co_names):
Expand Down
Loading
Loading